1use std::mem::size_of;
30
31use crate::bounds::{
32 checked_add, checked_capacity, checked_mul, checked_range, decoded_len, encode_input_len,
33 encode_u32_len, u32_to_usize,
34};
35use crate::error::CodecError;
36
37const MAX_SYMBOLS: usize = 255;
39
40const MAX_SYMBOL_LEN: usize = 8;
42
43const ESCAPE: u8 = 255;
45
46const TRAINING_ROUNDS: usize = 5;
48
49#[derive(Debug, Clone)]
55struct SymbolTable {
56 symbols: Vec<Vec<u8>>,
58}
59
60impl SymbolTable {
61 fn train(strings: &[&[u8]]) -> Self {
66 if strings.is_empty() {
67 return Self {
68 symbols: Vec::new(),
69 };
70 }
71
72 let mut symbols: Vec<Vec<u8>> = Vec::new();
73 let mut symbol_set: std::collections::HashSet<Vec<u8>> = std::collections::HashSet::new();
74 let mut candidates: std::collections::HashMap<Vec<u8>, usize> =
75 std::collections::HashMap::new();
76
77 for _round in 0..TRAINING_ROUNDS {
78 candidates.clear();
80
81 for s in strings {
82 let mut pos = 0;
84 while pos < s.len() {
85 let existing_match = longest_symbol_match(&symbols, s, pos);
87
88 if existing_match > 0 {
89 pos += existing_match;
90 continue;
91 }
92
93 for len in 1..=MAX_SYMBOL_LEN.min(s.len() - pos) {
95 let ngram = &s[pos..pos + len];
96 *candidates.entry(ngram.to_vec()).or_insert(0) += 1;
97 }
98 pos += 1;
99 }
100 }
101
102 if candidates.is_empty() {
103 break;
104 }
105
106 let mut scored: Vec<(Vec<u8>, usize)> = candidates
110 .drain()
111 .map(|(ngram, freq)| {
112 let gain = freq * (ngram.len().saturating_sub(1));
113 (ngram, gain)
114 })
115 .filter(|(_, gain)| *gain > 0)
116 .collect();
117
118 scored.sort_by_key(|a| std::cmp::Reverse(a.1));
119
120 for (ngram, _) in scored {
122 if symbols.len() >= MAX_SYMBOLS {
123 break;
124 }
125 if symbol_set.insert(ngram.clone()) {
126 symbols.push(ngram);
127 }
128 }
129 }
130
131 symbols.sort_by_key(|a| std::cmp::Reverse(a.len()));
133
134 Self { symbols }
135 }
136
137 fn symbol_count(&self) -> usize {
138 self.symbols.len()
139 }
140}
141
142fn longest_symbol_match(symbols: &[Vec<u8>], data: &[u8], pos: usize) -> usize {
145 let remaining = &data[pos..];
146 for sym in symbols {
147 if remaining.starts_with(sym) {
148 return sym.len();
149 }
150 }
151 0
152}
153
154pub fn encode(strings: &[&[u8]]) -> Result<Vec<u8>, CodecError> {
163 encode_input_len(strings.len(), "FSST string count")?;
164 let input_bytes = strings.iter().try_fold(0usize, |total, string| {
165 checked_add(total, string.len(), "FSST input bytes")
166 })?;
167 decoded_len(input_bytes, "FSST input")?;
168 let table = SymbolTable::train(strings);
169
170 let mut encoded_strings: Vec<Vec<u8>> = Vec::with_capacity(strings.len());
172 for s in strings {
173 encoded_strings.push(encode_string(&table, s)?);
174 }
175
176 let total_encoded = encoded_strings.iter().try_fold(0usize, |total, string| {
179 checked_add(total, string.len(), "FSST encoded bytes")
180 })?;
181 decoded_len(total_encoded, "FSST encoded bytes")?;
182 let total_encoded_u32 = encode_u32_len(total_encoded, "FSST encoded bytes")?;
183 let offset_bytes = checked_mul(strings.len(), 4, "FSST offsets")?;
184 decoded_len(offset_bytes, "FSST offsets")?;
185 let symbol_bytes = table.symbols.iter().try_fold(0usize, |total, symbol| {
186 checked_add(
187 checked_add(total, 1, "FSST symbols")?,
188 symbol.len(),
189 "FSST symbols",
190 )
191 })?;
192 let output_len = checked_add(
193 checked_add(
194 checked_add(2, symbol_bytes, "FSST output")?,
195 8,
196 "FSST output",
197 )?,
198 checked_add(offset_bytes, total_encoded, "FSST output")?,
199 "FSST output",
200 )?;
201 decoded_len(output_len, "FSST output")?;
202 let string_count = encode_input_len(strings.len(), "FSST string count")?;
203 let mut out = Vec::with_capacity(output_len);
204
205 out.extend_from_slice(&(table.symbol_count() as u16).to_le_bytes());
207 for sym in &table.symbols {
208 out.push(sym.len() as u8);
209 out.extend_from_slice(sym);
210 }
211 out.extend_from_slice(&total_encoded_u32.to_le_bytes());
212 out.extend_from_slice(&string_count.to_le_bytes());
213
214 let mut offset = 0usize;
216 for es in &encoded_strings {
217 offset = checked_add(offset, es.len(), "FSST string offset")?;
218 out.extend_from_slice(&encode_u32_len(offset, "FSST string offset")?.to_le_bytes());
219 }
220
221 for es in &encoded_strings {
223 out.extend_from_slice(es);
224 }
225
226 Ok(out)
227}
228
229pub fn decode(data: &[u8]) -> Result<Vec<Vec<u8>>, CodecError> {
231 if data.len() < 2 {
232 return Err(CodecError::Truncated {
233 expected: 2,
234 actual: data.len(),
235 });
236 }
237
238 let sym_count = usize::from(u16::from_le_bytes([data[0], data[1]]));
240 if sym_count > MAX_SYMBOLS {
241 return Err(CodecError::Corrupt {
242 detail: "FSST symbol count exceeds maximum".into(),
243 });
244 }
245 let mut pos = 2;
246 let symbol_capacity = checked_capacity(sym_count, size_of::<Vec<u8>>(), "FSST symbols")?;
247 let mut symbols: Vec<Vec<u8>> = Vec::with_capacity(symbol_capacity);
248
249 for _ in 0..sym_count {
250 let len = usize::from(checked_range(data, pos, 1, "FSST symbol length")?[0]);
251 pos = checked_add(pos, 1, "FSST symbol cursor")?;
252 if len == 0 || len > MAX_SYMBOL_LEN {
253 return Err(CodecError::Corrupt {
254 detail: "FSST symbol length is invalid".into(),
255 });
256 }
257 let symbol = checked_range(data, pos, len, "FSST symbol")?;
258 if symbols.iter().any(|existing| existing == symbol) {
259 return Err(CodecError::Corrupt {
260 detail: "duplicate FSST symbol".into(),
261 });
262 }
263 symbols.push(symbol.to_vec());
264 pos = checked_add(pos, len, "FSST symbol cursor")?;
265 }
266
267 let header = checked_range(data, pos, 8, "FSST header")?;
269 let total_encoded = u32_to_usize(
270 u32::from_le_bytes([header[0], header[1], header[2], header[3]]),
271 "FSST encoded length",
272 )?;
273 let string_count = u32_to_usize(
274 u32::from_le_bytes([header[4], header[5], header[6], header[7]]),
275 "FSST string count",
276 )?;
277 decoded_len(total_encoded, "FSST")?;
278 let container_bytes = checked_add(
279 checked_mul(string_count, size_of::<usize>(), "FSST offset allocation")?,
280 checked_mul(string_count, size_of::<Vec<u8>>(), "FSST result allocation")?,
281 "FSST container allocations",
282 )?;
283 decoded_len(container_bytes, "FSST containers")?;
284 pos = checked_add(pos, 8, "FSST header cursor")?;
285
286 let offsets_size = checked_mul(string_count, 4, "FSST offsets")?;
288 decoded_len(offsets_size, "FSST offsets")?;
289 let offsets_data = checked_range(data, pos, offsets_size, "FSST offsets")?;
290 let offset_capacity = checked_capacity(string_count, size_of::<usize>(), "FSST offsets")?;
291 let mut offsets = Vec::with_capacity(offset_capacity);
292 for i in 0..string_count {
293 let off_pos = checked_mul(i, 4, "FSST offset position")?;
294 offsets.push(u32_to_usize(
295 u32::from_le_bytes([
296 offsets_data[off_pos],
297 offsets_data[off_pos + 1],
298 offsets_data[off_pos + 2],
299 offsets_data[off_pos + 3],
300 ]),
301 "FSST string offset",
302 )?);
303 }
304 pos = checked_add(pos, offsets_size, "FSST data cursor")?;
305 let encoded_data = checked_range(data, pos, total_encoded, "FSST encoded data")?;
306 if checked_add(pos, total_encoded, "FSST frame end")? != data.len() {
307 return Err(CodecError::Corrupt {
308 detail: "trailing bytes after FSST frame".into(),
309 });
310 }
311
312 let result_capacity = checked_capacity(string_count, size_of::<Vec<u8>>(), "FSST results")?;
314 let mut result = Vec::with_capacity(result_capacity);
315 let mut prev_end = 0;
316 let mut decoded_total = 0usize;
317 for &end in &offsets {
318 if end < prev_end || end > encoded_data.len() {
319 return Err(CodecError::Corrupt {
320 detail: "FSST string offsets are not monotonic and in range".into(),
321 });
322 }
323 let encoded_str = &encoded_data[prev_end..end];
324 let decoded = decode_string(&symbols, encoded_str)?;
325 decoded_total = checked_add(decoded_total, decoded.len(), "FSST decoded bytes")?;
326 decoded_len(decoded_total, "FSST")?;
327 result.push(decoded);
328 prev_end = end;
329 }
330 if prev_end != total_encoded {
331 return Err(CodecError::Corrupt {
332 detail: "FSST offsets do not consume encoded data".into(),
333 });
334 }
335
336 Ok(result)
337}
338
339pub fn encode_delimited(data: &[u8], delimiter: u8) -> Result<Vec<u8>, CodecError> {
342 decoded_len(data.len(), "FSST delimited input")?;
343 let strings: Vec<&[u8]> = data.split(|&b| b == delimiter).collect();
344 encode(&strings)
345}
346
347pub fn decode_delimited(data: &[u8], delimiter: u8) -> Result<Vec<u8>, CodecError> {
349 let strings = decode(data)?;
350 let mut out = Vec::new();
351 for (i, s) in strings.iter().enumerate() {
352 let separator = usize::from(i > 0);
353 let next_len = checked_add(
354 checked_add(out.len(), separator, "FSST delimited output")?,
355 s.len(),
356 "FSST delimited output",
357 )?;
358 decoded_len(next_len, "FSST delimited output")?;
359 if i > 0 {
360 out.push(delimiter);
361 }
362 out.extend_from_slice(s);
363 }
364 Ok(out)
365}
366
367fn encode_string(table: &SymbolTable, input: &[u8]) -> Result<Vec<u8>, CodecError> {
372 let capacity = checked_mul(input.len(), 2, "FSST encoded string")?;
373 decoded_len(capacity, "FSST encoded string")?;
374 let mut out = Vec::with_capacity(capacity);
375 let mut pos = 0;
376
377 while pos < input.len() {
378 let mut matched = false;
380 for (idx, sym) in table.symbols.iter().enumerate() {
381 if input[pos..].starts_with(sym) {
382 out.push(idx as u8);
383 pos += sym.len();
384 matched = true;
385 break;
386 }
387 }
388
389 if !matched {
390 out.push(ESCAPE);
392 out.push(input[pos]);
393 pos += 1;
394 }
395 }
396
397 Ok(out)
398}
399
400fn decode_string(symbols: &[Vec<u8>], encoded: &[u8]) -> Result<Vec<u8>, CodecError> {
401 let expanded_capacity = checked_mul(encoded.len(), MAX_SYMBOL_LEN, "FSST decoded string")?;
402 let capacity = decoded_len(expanded_capacity, "FSST decoded string")?;
403 let mut out = Vec::with_capacity(capacity);
404 let mut pos = 0;
405
406 while pos < encoded.len() {
407 let byte = encoded[pos];
408 pos += 1;
409
410 if byte == ESCAPE {
411 if pos >= encoded.len() {
413 return Err(CodecError::Corrupt {
414 detail: "FSST escape at end of encoded data".into(),
415 });
416 }
417 out.push(encoded[pos]);
418 pos += 1;
419 } else {
420 let idx = byte as usize;
422 if idx >= symbols.len() {
423 return Err(CodecError::Corrupt {
424 detail: format!(
425 "FSST symbol index {idx} out of range (max {})",
426 symbols.len()
427 ),
428 });
429 }
430 let next_len = checked_add(out.len(), symbols[idx].len(), "FSST decoded string")?;
431 decoded_len(next_len, "FSST decoded string")?;
432 out.extend_from_slice(&symbols[idx]);
433 }
434 }
435
436 Ok(out)
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442
443 fn encode(strings: &[&[u8]]) -> Vec<u8> {
444 super::encode(strings).expect("test FSST encode")
445 }
446
447 fn encode_delimited(data: &[u8], delimiter: u8) -> Vec<u8> {
448 super::encode_delimited(data, delimiter).expect("test FSST delimited encode")
449 }
450
451 #[test]
452 fn empty_input() {
453 let encoded = encode(&[]);
454 let decoded = decode(&encoded).unwrap();
455 assert!(decoded.is_empty());
456 }
457
458 #[test]
459 fn single_string() {
460 let strings: Vec<&[u8]> = vec![b"hello world"];
461 let encoded = encode(&strings);
462 let decoded = decode(&encoded).unwrap();
463 assert_eq!(decoded.len(), 1);
464 assert_eq!(decoded[0], b"hello world");
465 }
466
467 #[test]
468 fn multiple_strings_roundtrip() {
469 let strings: Vec<&[u8]> = vec![
470 b"us-east-1",
471 b"us-east-2",
472 b"us-west-1",
473 b"eu-west-1",
474 b"us-east-1",
475 b"us-east-1",
476 ];
477 let encoded = encode(&strings);
478 let decoded = decode(&encoded).unwrap();
479 assert_eq!(decoded.len(), strings.len());
480 for (a, b) in strings.iter().zip(decoded.iter()) {
481 assert_eq!(*a, b.as_slice());
482 }
483 }
484
485 #[test]
486 fn repetitive_log_lines() {
487 let lines: Vec<&[u8]> = (0..1000)
488 .map(|i| {
489 let s: &[u8] = match i % 5 {
490 0 => b"2024-01-15 INFO server.handler request_id=abc method=GET status=200",
491 1 => b"2024-01-15 INFO server.handler request_id=def method=POST status=201",
492 2 => b"2024-01-15 WARN server.handler request_id=ghi method=GET status=404",
493 3 => b"2024-01-15 ERROR server.handler request_id=jkl method=PUT status=500",
494 _ => b"2024-01-15 DEBUG server.handler request_id=mno method=GET status=200",
495 };
496 s
497 })
498 .collect();
499
500 let encoded = encode(&lines);
501 let decoded = decode(&encoded).unwrap();
502 assert_eq!(decoded.len(), lines.len());
503 for (a, b) in lines.iter().zip(decoded.iter()) {
504 assert_eq!(*a, b.as_slice());
505 }
506
507 let raw_size: usize = lines.iter().map(|s| s.len()).sum();
509 let ratio = raw_size as f64 / encoded.len() as f64;
510 assert!(
511 ratio > 1.5,
512 "FSST should compress repetitive logs >1.5x, got {ratio:.1}x"
513 );
514 }
515
516 #[test]
517 fn hostnames() {
518 let hosts: Vec<&[u8]> = vec![
519 b"prod-web-01.us-east-1.example.com",
520 b"prod-web-02.us-east-1.example.com",
521 b"prod-web-03.us-east-1.example.com",
522 b"prod-api-01.us-west-2.example.com",
523 b"prod-api-02.us-west-2.example.com",
524 b"staging-web-01.eu-west-1.example.com",
525 ];
526 let encoded = encode(&hosts);
527 let decoded = decode(&encoded).unwrap();
528 for (a, b) in hosts.iter().zip(decoded.iter()) {
529 assert_eq!(*a, b.as_slice());
530 }
531 }
532
533 #[test]
534 fn binary_data() {
535 let data: Vec<&[u8]> = vec![&[0, 1, 2, 3, 4, 255, 254, 253]];
537 let encoded = encode(&data);
538 let decoded = decode(&encoded).unwrap();
539 assert_eq!(decoded[0], data[0]);
540 }
541
542 #[test]
543 fn empty_strings() {
544 let strings: Vec<&[u8]> = vec![b"", b"hello", b"", b"world", b""];
545 let encoded = encode(&strings);
546 let decoded = decode(&encoded).unwrap();
547 assert_eq!(decoded.len(), 5);
548 assert!(decoded[0].is_empty());
549 assert_eq!(decoded[1], b"hello");
550 assert!(decoded[2].is_empty());
551 }
552
553 #[test]
554 fn delimited_roundtrip() {
555 let data = b"line one\nline two\nline three\nline one\nline two";
556 let encoded = encode_delimited(data, b'\n');
557 let decoded = decode_delimited(&encoded, b'\n').unwrap();
558 assert_eq!(decoded, data);
559 }
560
561 #[test]
562 fn compression_ratio_structured_logs() {
563 let mut lines: Vec<Vec<u8>> = Vec::new();
564 for i in 0..5000 {
565 let line = format!(
566 "2024-01-15T10:30:{:02}.000Z INFO server.handler request_id={} method=GET path=/api/v1/metrics status=200 duration_ms={}",
567 i % 60,
568 10000 + i,
569 i * 3 + 1
570 );
571 lines.push(line.into_bytes());
572 }
573 let refs: Vec<&[u8]> = lines.iter().map(|l| l.as_slice()).collect();
574
575 let encoded = encode(&refs);
576 let decoded = decode(&encoded).unwrap();
577 assert_eq!(decoded.len(), lines.len());
578
579 let raw_size: usize = lines.iter().map(|s| s.len()).sum();
580 let ratio = raw_size as f64 / encoded.len() as f64;
581 assert!(
582 ratio > 1.5,
583 "FSST should compress structured logs >1.5x, got {ratio:.1}x"
584 );
585 }
586
587 #[test]
588 fn malformed_sections_and_trailing_bytes_are_rejected() {
589 assert!(decode(&[]).is_err());
590 assert!(decode(&[1]).is_err());
591 let mut encoded = encode(&[b"hello".as_slice()]);
592 encoded.push(0);
593 assert!(matches!(decode(&encoded), Err(CodecError::Corrupt { .. })));
594 let mut invalid_offset = encode(&[b"hello".as_slice()]);
595 let symbol_count = usize::from(u16::from_le_bytes([invalid_offset[0], invalid_offset[1]]));
596 let mut cursor = 2;
597 for _ in 0..symbol_count {
598 cursor += 1 + usize::from(invalid_offset[cursor]);
599 }
600 let offsets_start = cursor + 8;
601 invalid_offset[offsets_start..offsets_start + 4].copy_from_slice(&0u32.to_le_bytes());
604 assert!(matches!(
605 decode(&invalid_offset),
606 Err(CodecError::Corrupt { .. })
607 ));
608 }
609
610 #[test]
611 fn large_dataset() {
612 let mut strings: Vec<Vec<u8>> = Vec::new();
613 for i in 0..10_000 {
614 strings.push(format!("key-{}-value-{}", i % 100, i % 50).into_bytes());
615 }
616 let refs: Vec<&[u8]> = strings.iter().map(|s| s.as_slice()).collect();
617 let encoded = encode(&refs);
618 let decoded = decode(&encoded).unwrap();
619 assert_eq!(decoded.len(), strings.len());
620 for (a, b) in strings.iter().zip(decoded.iter()) {
621 assert_eq!(a.as_slice(), b.as_slice());
622 }
623 }
624}