1use std::cell::RefCell;
46use std::sync::OnceLock;
47
48use rudb_common::{Error, Result};
49
50pub const ESCAPE: u8 = 255;
53
54pub const MAX_SYMBOLS: usize = 255;
56
57pub const MAX_SYMBOL_LEN: usize = 8;
60
61const GENERATIONS: usize = 5;
63
64const HASH_SLOTS: usize = 1024;
67
68const PROBE: usize = 8;
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
74struct Symbol {
75 value: u64,
76 len: u8,
77}
78
79impl Symbol {
80 fn new(bytes: &[u8]) -> Self {
81 let len = bytes.len().min(MAX_SYMBOL_LEN);
82 let mut value = 0u64;
83 for (index, byte) in bytes[..len].iter().enumerate() {
84 value |= u64::from(*byte) << (8 * index);
85 }
86 Self { value, len: len as u8 }
87 }
88
89 fn single(byte: u8) -> Self {
90 Self { value: u64::from(byte), len: 1 }
91 }
92
93 fn len(self) -> usize {
94 self.len as usize
95 }
96
97 fn mask(self) -> u64 {
98 mask_of(self.len())
99 }
100
101 fn bytes(self) -> Vec<u8> {
102 (0..self.len()).map(|index| (self.value >> (8 * index)) as u8).collect()
103 }
104
105 fn concat(self, other: Self) -> Self {
107 if self.len() >= MAX_SYMBOL_LEN {
108 return self;
109 }
110 let len = (self.len() + other.len()).min(MAX_SYMBOL_LEN);
111 let value = self.value | (other.value << (8 * self.len()));
112 Self { value: value & mask_of(len), len: len as u8 }
113 }
114}
115
116fn mask_of(len: usize) -> u64 {
117 if len >= 8 { u64::MAX } else { (1u64 << (8 * len)) - 1 }
118}
119
120pub struct SymbolTable {
122 symbols: Vec<Symbol>,
124 lookup: OnceLock<Lookup>,
131}
132
133struct Lookup {
135 single: Vec<u8>,
138 short: Vec<u16>,
143 heads: Vec<Head>,
146 long: Vec<(Symbol, u8)>,
150}
151
152#[derive(Debug, Clone, Copy)]
154struct Head {
155 key: u32,
157 first: u16,
159 count: u16,
160}
161
162const NO_HEAD: u32 = u32::MAX;
164
165impl std::fmt::Debug for SymbolTable {
166 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167 formatter
169 .debug_struct("SymbolTable")
170 .field("symbols", &self.symbols.len())
171 .field("bytes", &self.serialized_len())
172 .finish()
173 }
174}
175
176impl PartialEq for SymbolTable {
183 fn eq(&self, other: &Self) -> bool {
184 self.symbols == other.symbols
185 }
186}
187
188impl Eq for SymbolTable {}
189
190impl SymbolTable {
191 #[must_use]
198 pub fn footprint(&self) -> usize {
199 size_of::<Self>()
200 + self.symbols.capacity() * size_of::<Symbol>()
201 + self.lookup.get().map_or(0, |lookup| {
202 lookup.single.capacity()
203 + lookup.short.capacity() * size_of::<u16>()
204 + lookup.heads.capacity() * size_of::<Head>()
205 + lookup.long.capacity() * size_of::<(Symbol, u8)>()
206 })
207 }
208
209 #[must_use]
212 pub fn empty() -> Self {
213 Self::build(Vec::new())
214 }
215
216 #[must_use]
222 pub fn train(samples: &[&[u8]]) -> Self {
223 thread_local! {
227 static COUNTS: RefCell<Option<Counts>> = const { RefCell::new(None) };
228 }
229 COUNTS.with(|held| match held.try_borrow_mut() {
230 Ok(mut held) => Self::train_with(samples, held.get_or_insert_with(Counts::new)),
231 Err(_) => Self::train_with(samples, &mut Counts::new()),
232 })
233 }
234
235 fn train_with(samples: &[&[u8]], counts: &mut Counts) -> Self {
236 let mut table = Self::empty();
237 for _ in 0..GENERATIONS {
238 counts.clear();
239 for sample in samples {
240 table.count(sample, counts);
241 }
242 let next = counts.best(&table);
243 if next.is_empty() {
244 break;
245 }
246 table = Self::build(next);
247 }
248 table
249 }
250
251 #[must_use]
253 pub fn len(&self) -> usize {
254 self.symbols.len()
255 }
256
257 #[must_use]
259 pub fn is_empty(&self) -> bool {
260 self.symbols.is_empty()
261 }
262
263 #[must_use]
269 pub fn symbol(&self, code: u8) -> Option<([u8; MAX_SYMBOL_LEN], usize)> {
270 let symbol = self.symbols.get(code as usize)?;
271 Some((symbol.value.to_le_bytes(), symbol.len()))
272 }
273
274 #[must_use]
278 pub fn serialized_len(&self) -> usize {
279 1 + self.symbols.iter().map(|symbol| 1 + symbol.len()).sum::<usize>()
280 }
281
282 pub fn serialize(&self, out: &mut Vec<u8>) {
284 out.push(self.symbols.len() as u8);
285 for symbol in &self.symbols {
286 out.push(symbol.len);
287 out.extend_from_slice(&symbol.bytes());
288 }
289 }
290
291 pub fn deserialize(bytes: &[u8]) -> Result<(Self, usize)> {
297 let count = *bytes.first().ok_or_else(|| truncated("a symbol table header"))? as usize;
298 let mut at = 1;
299 let mut symbols = Vec::with_capacity(count);
300 for _ in 0..count {
301 let len = *bytes.get(at).ok_or_else(|| truncated("a symbol length"))? as usize;
302 if len == 0 || len > MAX_SYMBOL_LEN {
303 return Err(Error::internal(format!("a symbol of {len} bytes is not a symbol")));
304 }
305 at += 1;
306 let end = at + len;
307 if end > bytes.len() {
308 return Err(truncated("a symbol"));
309 }
310 symbols.push(Symbol::new(&bytes[at..end]));
311 at = end;
312 }
313 Ok((Self::build(symbols), at))
314 }
315
316 pub fn compress(&self, input: &[u8], out: &mut Vec<u8>) {
322 let lookup = self.lookup();
323 let mut at = 0;
324 while at < input.len() {
325 let (code, len) = lookup.match_at(input, at);
326 if code == ESCAPE {
327 out.push(ESCAPE);
328 out.push(input[at]);
329 } else {
330 out.push(code);
331 }
332 at += len;
333 }
334 }
335
336 pub fn decompress(&self, input: &[u8], out: &mut Vec<u8>) -> Result<()> {
353 out.reserve(input.len().saturating_mul(2).saturating_add(MAX_SYMBOL_LEN));
357 let mut at = 0;
358 while at < input.len() {
359 let code = input[at];
360 at += 1;
361 if code == ESCAPE {
362 let literal = *input.get(at).ok_or_else(|| truncated("an escaped byte"))?;
363 out.push(literal);
364 at += 1;
365 } else {
366 let symbol = *self
367 .symbols
368 .get(code as usize)
369 .ok_or_else(|| Error::internal(format!("code {code} is not in the table")))?;
370 out.extend_from_slice(&symbol.value.to_le_bytes());
371 out.truncate(out.len() - (MAX_SYMBOL_LEN - symbol.len()));
372 }
373 }
374 Ok(())
375 }
376
377 #[inline]
392 pub fn decompress_at(&self, input: &[u8], out: &mut [u8], mut at: usize) -> Result<usize> {
393 let symbols = self.symbols.as_slice();
394 let mut codes = input.iter();
395 while let Some(&code) = codes.next() {
396 if code == ESCAPE {
397 let Some(&literal) = codes.next() else {
398 return Err(truncated("an escaped byte"));
399 };
400 let Some(slot) = out.get_mut(at) else {
401 return Err(out_of_room());
402 };
403 *slot = literal;
404 at += 1;
405 } else {
406 let Some(symbol) = symbols.get(code as usize) else {
407 return Err(not_in_table(code));
408 };
409 let Some(slot) = out.get_mut(at..at + MAX_SYMBOL_LEN) else {
410 return Err(out_of_room());
411 };
412 slot.copy_from_slice(&symbol.value.to_le_bytes());
413 at += symbol.len();
414 }
415 }
416 Ok(at)
417 }
418
419 fn count(&self, input: &[u8], counts: &mut Counts) {
422 let lookup = self.lookup();
423 let mut at = 0;
424 let mut previous: Option<u16> = None;
425 while at < input.len() {
426 let (code, len) = lookup.match_at(input, at);
427 let id = if code == ESCAPE { 256 + u16::from(input[at]) } else { u16::from(code) };
428 counts.one(id);
429 if let Some(previous) = previous {
430 counts.two(previous, id);
431 }
432 previous = Some(id);
433 at += len;
434 }
435 }
436
437 fn build(symbols: Vec<Symbol>) -> Self {
438 Self { symbols, lookup: OnceLock::new() }
439 }
440
441 fn lookup(&self) -> &Lookup {
442 self.lookup.get_or_init(|| Lookup::of(&self.symbols))
443 }
444}
445
446impl Lookup {
447 fn of(symbols: &[Symbol]) -> Self {
448 let mut single = vec![ESCAPE; 256];
449 let mut pair = vec![u16::MAX; 65536];
450 let mut placed: Vec<Option<(Symbol, u8)>> = vec![None; HASH_SLOTS];
456 let mut long = Vec::new();
457 let mut order: Vec<(Symbol, u8)> =
460 symbols.iter().enumerate().map(|(code, symbol)| (*symbol, code as u8)).collect();
461 order.sort_by_key(|(symbol, code)| (std::cmp::Reverse(symbol.len()), *code));
462 for (symbol, code) in order {
463 match symbol.len() {
464 1 => {
465 let index = (symbol.value & 0xff) as usize;
466 if single[index] == ESCAPE {
467 single[index] = code;
468 }
469 }
470 2 => {
471 let index = (symbol.value & 0xffff) as usize;
472 if pair[index] == u16::MAX {
473 pair[index] = u16::from(code);
474 }
475 }
476 _ => {
477 let mut slot = hash_of(symbol.value);
478 for _ in 0..PROBE {
479 if placed[slot].is_none() {
480 placed[slot] = Some((symbol, code));
481 long.push((symbol, code));
482 break;
483 }
484 slot = (slot + 1) & (HASH_SLOTS - 1);
485 }
486 }
487 }
488 }
489 long.sort_by_key(|(symbol, _)| symbol.value & 0xff_ffff);
493 let mut heads = vec![Head { key: NO_HEAD, first: 0, count: 0 }; HASH_SLOTS];
494 let mut start = 0;
495 while start < long.len() {
496 let key = long[start].0.value & 0xff_ffff;
497 let mut end = start + 1;
498 while end < long.len() && long[end].0.value & 0xff_ffff == key {
499 end += 1;
500 }
501 let mut slot = hash_of(key);
504 while heads[slot].key != NO_HEAD {
505 slot = (slot + 1) & (HASH_SLOTS - 1);
506 }
507 heads[slot] =
508 Head { key: key as u32, first: start as u16, count: (end - start) as u16 };
509 start = end;
510 }
511 let short = (0..65536usize)
512 .map(|index| {
513 if pair[index] == u16::MAX {
514 u16::from(single[index & 0xff]) | 1 << 8
515 } else {
516 pair[index] | 2 << 8
517 }
518 })
519 .collect();
520 Self { single, short, heads, long }
521 }
522
523 #[inline(always)]
528 fn match_at(&self, input: &[u8], at: usize) -> (u8, usize) {
529 let remaining = input.len() - at;
530 let word = load(input, at);
531 if remaining >= 3
534 && let Some(found) = self.long_at(word, remaining)
535 {
536 return found;
537 }
538 if remaining >= 2 {
539 let entry = self.short[(word & 0xffff) as usize];
540 return ((entry & 0xff) as u8, usize::from(entry >> 8));
541 }
542 (self.single[(word & 0xff) as usize], 1)
543 }
544
545 #[inline]
551 fn long_at(&self, word: u64, remaining: usize) -> Option<(u8, usize)> {
552 let key = (word & 0xff_ffff) as u32;
553 let mut slot = hash_of(word);
554 loop {
555 let head = self.heads[slot];
556 if head.key == key {
557 let group = &self.long[usize::from(head.first)..][..usize::from(head.count)];
558 return group
559 .iter()
560 .find(|(symbol, _)| {
561 symbol.len() <= remaining && word & symbol.mask() == symbol.value
562 })
563 .map(|(symbol, code)| (*code, symbol.len()));
564 }
565 if head.key == NO_HEAD {
566 return None;
567 }
568 slot = (slot + 1) & (HASH_SLOTS - 1);
569 }
570 }
571}
572
573#[inline]
582fn load(input: &[u8], at: usize) -> u64 {
583 if at + 8 <= input.len() {
584 let bytes: [u8; 8] = input[at..at + 8].try_into().expect("eight bytes were checked");
585 u64::from_le_bytes(bytes)
586 } else if input.len() >= 8 {
587 let bytes: [u8; 8] = input[input.len() - 8..].try_into().expect("eight bytes were checked");
588 u64::from_le_bytes(bytes) >> (8 * (at + 8 - input.len()))
591 } else {
592 let mut word = 0u64;
593 for (index, byte) in input[at..].iter().enumerate() {
594 word |= u64::from(*byte) << (8 * index);
595 }
596 word
597 }
598}
599
600fn hash_of(word: u64) -> usize {
604 let key = word & 0xff_ffff;
605 ((key.wrapping_mul(0x9e37_79b9_7f4a_7c15)) >> (64 - HASH_SLOTS.trailing_zeros())) as usize
606}
607
608struct Counts {
612 single: Vec<u32>,
613 pairs: Vec<u32>,
616 seen: Vec<u32>,
619 gains: Gains,
621}
622
623#[derive(Default)]
625struct Gains {
626 gains: Vec<(Symbol, u64)>,
627 places: Vec<u32>,
629 taken: Vec<u32>,
631}
632
633impl Gains {
634 fn clear(&mut self, most: usize) {
636 for place in self.taken.drain(..) {
637 self.places[place as usize] = u32::MAX;
638 }
639 let wanted = (most * 2).next_power_of_two();
640 if self.places.len() < wanted {
641 self.places = vec![u32::MAX; wanted];
642 }
643 self.gains.clear();
644 }
645
646 fn add(&mut self, symbol: Symbol, count: u64) {
648 let gain = count * symbol.len() as u64;
649 let mask = self.places.len() - 1;
650 let mut place = gain_hash(symbol) & mask;
651 loop {
652 let at = self.places[place];
653 if at == u32::MAX {
654 self.places[place] = self.gains.len() as u32;
655 self.taken.push(place as u32);
656 self.gains.push((symbol, gain));
657 return;
658 }
659 if self.gains[at as usize].0 == symbol {
660 self.gains[at as usize].1 += gain;
661 return;
662 }
663 place = (place + 1) & mask;
664 }
665 }
666}
667
668const IDS: usize = 512;
670
671impl Counts {
672 fn new() -> Self {
673 Self {
674 single: vec![0; IDS],
675 pairs: vec![0; IDS * IDS],
676 seen: Vec::new(),
677 gains: Gains::default(),
678 }
679 }
680
681 fn clear(&mut self) {
682 self.single.fill(0);
683 for slot in self.seen.drain(..) {
684 self.pairs[slot as usize] = 0;
685 }
686 }
687
688 fn one(&mut self, id: u16) {
689 self.single[id as usize] += 1;
690 }
691
692 fn two(&mut self, first: u16, second: u16) {
693 let slot = first as usize * IDS + second as usize;
694 if self.pairs[slot] == 0 {
695 self.seen.push(slot as u32);
696 }
697 self.pairs[slot] += 1;
698 }
699
700 fn best(&mut self, table: &SymbolTable) -> Vec<Symbol> {
707 self.gains.clear(IDS + self.seen.len());
712 for (id, count) in self.single.iter().enumerate() {
713 if *count == 0 {
714 continue;
715 }
716 let symbol = symbol_of(table, id as u16);
717 self.gains.add(symbol, u64::from(*count));
718 }
719 for slot in &self.seen {
720 let slot = *slot as usize;
721 let (first, second) = ((slot / IDS) as u16, (slot % IDS) as u16);
722 let symbol = symbol_of(table, first).concat(symbol_of(table, second));
723 self.gains.add(symbol, u64::from(self.pairs[slot]));
724 }
725 let gains = &mut self.gains.gains;
726 let order = |left: &(Symbol, u64), right: &(Symbol, u64)| {
730 right.1.cmp(&left.1).then(left.0.cmp(&right.0))
731 };
732 if gains.len() > MAX_SYMBOLS {
733 gains.select_nth_unstable_by(MAX_SYMBOLS - 1, order);
734 gains.truncate(MAX_SYMBOLS);
735 }
736 gains.sort_unstable_by(order);
737 gains.iter().map(|(symbol, _)| *symbol).collect()
738 }
739}
740
741fn gain_hash(symbol: Symbol) -> usize {
743 ((symbol.value ^ u64::from(symbol.len)).wrapping_mul(0x9e37_79b9_7f4a_7c15) >> 32) as usize
744}
745
746fn symbol_of(table: &SymbolTable, id: u16) -> Symbol {
747 if (id as usize) < table.symbols.len() {
748 table.symbols[id as usize]
749 } else {
750 Symbol::single((id.saturating_sub(256)) as u8)
751 }
752}
753
754#[cold]
755fn not_in_table(code: u8) -> Error {
756 Error::internal(format!("code {code} is not in the table"))
757}
758
759#[cold]
760fn out_of_room() -> Error {
761 Error::internal("a string decompresses to more than its length says")
762}
763
764#[cold]
765fn truncated(what: &str) -> Error {
766 Error::internal(format!("the input ended in the middle of {what}"))
767}
768
769#[cfg(test)]
770mod tests {
771 use super::*;
772
773 #[test]
774 fn a_table_read_back_to_decompress_builds_no_lookup_tables() {
775 let urls = urls();
776 let samples: Vec<&[u8]> = urls.iter().map(Vec::as_slice).collect();
777 let trained = SymbolTable::train(&samples);
778 let mut compressed = Vec::new();
779 trained.compress(&urls[7], &mut compressed);
780 let mut stored = Vec::new();
781 trained.serialize(&mut stored);
782 let (read, _) = SymbolTable::deserialize(&stored).expect("a table it wrote");
783 let mut out = Vec::new();
784 read.decompress(&compressed, &mut out).expect("a string it compressed");
785 assert_eq!(out, urls[7]);
786 assert!(read.lookup.get().is_none(), "decompressing reads the symbols alone");
787 assert!(read.footprint() < trained.footprint());
788 }
789
790 fn urls() -> Vec<Vec<u8>> {
794 let hosts = ["www.example.com", "shop.example.com", "news.other.example.org"];
795 let paths = ["/index.html", "/catalog/item", "/search", "/user/profile/settings"];
796 let mut out = Vec::new();
797 for index in 0..600 {
798 let host = hosts[index % hosts.len()];
799 let path = paths[(index / 3) % paths.len()];
800 out.push(
801 format!("http://{host}{path}?session={}&ref=google&page={}", index * 7, index % 20)
802 .into_bytes(),
803 );
804 }
805 out
806 }
807
808 fn borrow(strings: &[Vec<u8>]) -> Vec<&[u8]> {
809 strings.iter().map(Vec::as_slice).collect()
810 }
811
812 fn round_trip(table: &SymbolTable, strings: &[Vec<u8>]) -> (usize, usize) {
813 let mut raw = 0;
814 let mut compressed = 0;
815 for string in strings {
816 let mut bytes = Vec::new();
817 table.compress(string, &mut bytes);
818 let mut back = Vec::new();
819 table.decompress(&bytes, &mut back).unwrap();
820 assert_eq!(back, *string, "{}", String::from_utf8_lossy(string));
821 raw += string.len();
822 compressed += bytes.len();
823 }
824 (raw, compressed)
825 }
826
827 #[test]
828 fn urls_compress_by_more_than_half_and_come_back_unchanged() {
829 let strings = urls();
832 let table = SymbolTable::train(&borrow(&strings));
833 let (raw, compressed) = round_trip(&table, &strings);
834 let ratio = raw as f64 / compressed as f64;
835 assert!(ratio > 2.5, "{ratio:.2}x, {raw} to {compressed}");
836 assert!(table.len() > 100, "{} symbols", table.len());
837 }
838
839 #[test]
840 fn the_trainer_finds_the_long_repeated_pieces() {
841 let strings = urls();
842 let table = SymbolTable::train(&borrow(&strings));
843 let found: Vec<String> = (0..table.len())
844 .map(|code| String::from_utf8_lossy(&table.symbols[code].bytes()).into_owned())
845 .collect();
846 let long = found.iter().filter(|symbol| symbol.len() >= 6).count();
849 assert!(long > 60, "only {long} symbols of six bytes or more: {found:?}");
850 }
851
852 #[test]
853 fn english_text_round_trips_and_shrinks() {
854 let text: Vec<Vec<u8>> = "the quick brown fox jumps over the lazy dog while the other dog \
855 watches the fox and the dog and the fox go over the hill together"
856 .split(' ')
857 .map(|word| word.as_bytes().to_vec())
858 .collect();
859 let table = SymbolTable::train(&borrow(&text));
860 let (raw, compressed) = round_trip(&table, &text);
861 assert!(compressed < raw, "{raw} to {compressed}");
862 }
863
864 #[test]
865 fn incompressible_bytes_round_trip_and_cost_what_escaping_costs() {
866 let mut state = 0x1234_5678_9abc_def0u64;
870 let strings: Vec<Vec<u8>> = (0..100)
871 .map(|_| {
872 (0..64)
873 .map(|_| {
874 state ^= state << 13;
875 state ^= state >> 7;
876 state ^= state << 17;
877 state as u8
878 })
879 .collect()
880 })
881 .collect();
882 let table = SymbolTable::train(&borrow(&strings));
883 let (raw, compressed) = round_trip(&table, &strings);
884 assert!(compressed < raw * 2, "{raw} to {compressed}");
885 }
886
887 #[test]
888 fn an_empty_table_escapes_everything_and_still_round_trips() {
889 let table = SymbolTable::empty();
890 let strings = vec![b"hello".to_vec(), Vec::new(), b"x".to_vec()];
891 let (raw, compressed) = round_trip(&table, &strings);
892 assert_eq!(compressed, raw * 2);
893 }
894
895 #[test]
896 fn an_empty_string_compresses_to_nothing() {
897 let table = SymbolTable::train(&[b"abcabcabc"]);
898 let mut out = Vec::new();
899 table.compress(b"", &mut out);
900 assert!(out.is_empty());
901 let mut back = Vec::new();
902 table.decompress(&out, &mut back).unwrap();
903 assert!(back.is_empty());
904 }
905
906 #[test]
907 fn a_string_shorter_than_the_symbols_does_not_read_past_its_end() {
908 let table = SymbolTable::train(&[b"ab\0ab\0ab\0ab\0", b"abcdefgh"]);
911 for string in [b"a".to_vec(), b"ab".to_vec(), b"abc".to_vec()] {
912 let mut bytes = Vec::new();
913 table.compress(&string, &mut bytes);
914 let mut back = Vec::new();
915 table.decompress(&bytes, &mut back).unwrap();
916 assert_eq!(back, string);
917 }
918 }
919
920 #[test]
921 fn a_table_survives_being_written_and_read_back() {
922 let strings = urls();
923 let table = SymbolTable::train(&borrow(&strings));
924 let mut bytes = Vec::new();
925 table.serialize(&mut bytes);
926 assert_eq!(bytes.len(), table.serialized_len());
927 let (read, consumed) = SymbolTable::deserialize(&bytes).unwrap();
928 assert_eq!(consumed, bytes.len());
929 assert_eq!(read.symbols, table.symbols);
930
931 let mut first = Vec::new();
934 let mut second = Vec::new();
935 table.compress(&strings[7], &mut first);
936 read.compress(&strings[7], &mut second);
937 assert_eq!(first, second);
938 }
939
940 #[test]
941 fn a_full_table_is_two_kilobytes_at_the_very_most() {
942 let strings = urls();
943 let table = SymbolTable::train(&borrow(&strings));
944 assert!(table.serialized_len() <= 1 + MAX_SYMBOLS * (1 + MAX_SYMBOL_LEN));
945 assert!(table.serialized_len() <= 2049);
946 }
947
948 #[test]
949 fn a_truncated_symbol_table_is_an_error() {
950 let strings = urls();
951 let table = SymbolTable::train(&borrow(&strings));
952 let mut bytes = Vec::new();
953 table.serialize(&mut bytes);
954 for len in 1..bytes.len().min(40) {
955 let error = SymbolTable::deserialize(&bytes[..len]).unwrap_err();
956 assert!(error.message().contains("ended in the middle"), "{error}");
957 }
958 }
959
960 #[test]
961 fn a_symbol_of_zero_bytes_is_an_error() {
962 let error = SymbolTable::deserialize(&[1, 0]).unwrap_err();
963 assert!(error.message().contains("is not a symbol"), "{error}");
964 }
965
966 #[test]
967 fn a_short_symbol_does_not_drag_the_rest_of_its_word_out_with_it() {
968 let table = SymbolTable::train(&[b"abcdabcdabcdabcd"]);
974 let mut compressed = Vec::new();
975 table.compress(b"abcdabcd", &mut compressed);
976 let mut out = Vec::new();
977 table.decompress(&compressed, &mut out).expect("decompresses");
978 table.decompress(&compressed, &mut out).expect("decompresses");
979 assert_eq!(out, b"abcdabcdabcdabcd");
980 }
981
982 #[test]
983 fn a_dangling_escape_is_an_error_and_not_a_panic() {
984 let table = SymbolTable::train(&[b"abcabcabc"]);
985 let error = table.decompress(&[ESCAPE], &mut Vec::new()).unwrap_err();
986 assert!(error.message().contains("escaped byte"), "{error}");
987 }
988
989 #[test]
990 fn a_code_the_table_does_not_have_is_an_error() {
991 let table = SymbolTable::train(&[b"abcabcabc"]);
992 let code = table.len() as u8;
993 let error = table.decompress(&[code], &mut Vec::new()).unwrap_err();
994 assert!(error.message().contains("not in the table"), "{error}");
995 }
996
997 #[test]
998 fn training_twice_on_the_same_sample_gives_the_same_table() {
999 let strings = urls();
1001 let first = SymbolTable::train(&borrow(&strings));
1002 let second = SymbolTable::train(&borrow(&strings));
1003 assert_eq!(first.symbols, second.symbols);
1004 }
1005
1006 #[test]
1007 fn the_longest_match_wins_rather_than_the_first_one_found() {
1008 let table = SymbolTable::build(vec![
1009 Symbol::new(b"abc"),
1010 Symbol::new(b"abcdef"),
1011 Symbol::new(b"abcd"),
1012 ]);
1013 let mut out = Vec::new();
1014 table.compress(b"abcdef", &mut out);
1015 assert_eq!(out, vec![1]);
1016 }
1017
1018 #[test]
1019 fn a_symbol_longer_than_what_is_left_is_not_used() {
1020 let table = SymbolTable::build(vec![Symbol::new(b"abcdef"), Symbol::new(b"ab")]);
1021 let mut out = Vec::new();
1022 table.compress(b"abcd", &mut out);
1023 assert_eq!(out, vec![1, ESCAPE, b'c', ESCAPE, b'd']);
1025 }
1026
1027 fn match_by_probe(symbols: &[Symbol], input: &[u8], at: usize) -> (u8, usize) {
1031 let mut single = vec![ESCAPE; 256];
1032 let mut pair = vec![u16::MAX; 65536];
1033 let mut hash: Vec<Option<(Symbol, u8)>> = vec![None; HASH_SLOTS];
1034 let mut order: Vec<(Symbol, u8)> =
1035 symbols.iter().enumerate().map(|(code, symbol)| (*symbol, code as u8)).collect();
1036 order.sort_by_key(|(symbol, code)| (std::cmp::Reverse(symbol.len()), *code));
1037 for (symbol, code) in order {
1038 match symbol.len() {
1039 1 if single[(symbol.value & 0xff) as usize] == ESCAPE => {
1040 single[(symbol.value & 0xff) as usize] = code;
1041 }
1042 2 if pair[(symbol.value & 0xffff) as usize] == u16::MAX => {
1043 pair[(symbol.value & 0xffff) as usize] = u16::from(code);
1044 }
1045 1 | 2 => {}
1046 _ => {
1047 let mut slot = hash_of(symbol.value);
1048 for _ in 0..PROBE {
1049 if hash[slot].is_none() {
1050 hash[slot] = Some((symbol, code));
1051 break;
1052 }
1053 slot = (slot + 1) & (HASH_SLOTS - 1);
1054 }
1055 }
1056 }
1057 }
1058 let remaining = input.len() - at;
1059 let word = load(input, at);
1060 if remaining >= 3 {
1061 let mut slot = hash_of(word);
1062 let mut best: Option<(Symbol, u8)> = None;
1063 for _ in 0..PROBE {
1064 let Some((symbol, code)) = hash[slot] else { break };
1065 if symbol.len() <= remaining
1066 && word & symbol.mask() == symbol.value
1067 && best.is_none_or(|(found, _)| symbol.len() > found.len())
1068 {
1069 best = Some((symbol, code));
1070 }
1071 slot = (slot + 1) & (HASH_SLOTS - 1);
1072 }
1073 if let Some((symbol, code)) = best {
1074 return (code, symbol.len());
1075 }
1076 }
1077 if remaining >= 2 {
1078 let code = pair[(word & 0xffff) as usize];
1079 if code != u16::MAX {
1080 return (code as u8, 2);
1081 }
1082 }
1083 (single[(word & 0xff) as usize], 1)
1084 }
1085
1086 #[test]
1087 fn grouping_the_long_symbols_matches_what_the_probe_matched() {
1088 let alphabet = b"ab\0c";
1091 let mut seed = 0x2545_f491_4f6c_dd1du64;
1092 let mut next = move |below: usize| {
1093 seed ^= seed << 13;
1094 seed ^= seed >> 7;
1095 seed ^= seed << 17;
1096 (seed % below as u64) as usize
1097 };
1098 for round in 0..40 {
1099 let mut symbols = Vec::new();
1100 while symbols.len() < MAX_SYMBOLS {
1101 let len = 1 + next(MAX_SYMBOL_LEN);
1102 let bytes: Vec<u8> = (0..len).map(|_| alphabet[next(alphabet.len())]).collect();
1103 symbols.push(Symbol::new(&bytes));
1104 }
1105 let table = SymbolTable::build(symbols.clone());
1106 for _ in 0..50 {
1107 let input: Vec<u8> =
1108 (0..next(40)).map(|_| alphabet[next(alphabet.len())]).collect();
1109 for at in 0..input.len() {
1110 assert_eq!(
1111 table.lookup().match_at(&input, at),
1112 match_by_probe(&symbols, &input, at),
1113 "round {round}, {input:?} at {at}"
1114 );
1115 }
1116 }
1117 }
1118 }
1119
1120 #[test]
1121 fn concatenation_stops_at_eight_bytes() {
1122 let long = Symbol::new(b"abcdef");
1123 assert_eq!(long.concat(Symbol::new(b"ghijkl")).bytes(), b"abcdefgh");
1124 assert_eq!(Symbol::new(b"ab").concat(Symbol::new(b"cd")).bytes(), b"abcd");
1125 }
1126
1127 fn train_with_maps(samples: &[&[u8]]) -> SymbolTable {
1130 use std::collections::HashMap;
1131 let mut table = SymbolTable::empty();
1132 for _ in 0..GENERATIONS {
1133 let mut single = [0u32; IDS];
1134 let mut pairs: HashMap<(u16, u16), u32> = HashMap::new();
1135 for sample in samples {
1136 let mut at = 0;
1137 let mut previous: Option<u16> = None;
1138 while at < sample.len() {
1139 let (code, len) = table.lookup().match_at(sample, at);
1140 let id =
1141 if code == ESCAPE { 256 + u16::from(sample[at]) } else { u16::from(code) };
1142 single[id as usize] += 1;
1143 if let Some(previous) = previous {
1144 *pairs.entry((previous, id)).or_insert(0) += 1;
1145 }
1146 previous = Some(id);
1147 at += len;
1148 }
1149 }
1150 let mut gains: HashMap<Symbol, u64> = HashMap::new();
1151 for (id, count) in single.iter().enumerate().filter(|(_, count)| **count > 0) {
1152 let symbol = symbol_of(&table, id as u16);
1153 *gains.entry(symbol).or_insert(0) += u64::from(*count) * symbol.len() as u64;
1154 }
1155 for ((first, second), count) in &pairs {
1156 let symbol = symbol_of(&table, *first).concat(symbol_of(&table, *second));
1157 *gains.entry(symbol).or_insert(0) += u64::from(*count) * symbol.len() as u64;
1158 }
1159 let mut ranked: Vec<(Symbol, u64)> = gains.into_iter().collect();
1160 ranked.sort_by(|left, right| right.1.cmp(&left.1).then(left.0.cmp(&right.0)));
1161 ranked.truncate(MAX_SYMBOLS);
1162 if ranked.is_empty() {
1163 break;
1164 }
1165 table = SymbolTable::build(ranked.into_iter().map(|(symbol, _)| symbol).collect());
1166 }
1167 table
1168 }
1169
1170 #[test]
1171 fn flat_counts_train_the_same_table_as_hash_maps() {
1172 let mut state = 0x9e37_79b9_7f4a_7c15u64;
1173 let mut next = move || {
1174 state ^= state << 13;
1175 state ^= state >> 7;
1176 state ^= state << 17;
1177 state
1178 };
1179 let mut shapes: Vec<Vec<Vec<u8>>> = vec![urls(), Vec::new(), vec![Vec::new(); 3]];
1180 shapes.push(
1182 (0..400).map(|_| (0..12).map(|_| b"abc"[(next() % 3) as usize]).collect()).collect(),
1183 );
1184 shapes.push((0..300).map(|_| (0..40).map(|_| next() as u8).collect()).collect());
1186 shapes.push(
1188 (0..200)
1189 .map(|index| format!("prefix-{}-suffix-{}", index % 7, index % 5).into_bytes())
1190 .collect(),
1191 );
1192 for strings in &shapes {
1195 let samples = borrow(strings);
1196 assert_eq!(SymbolTable::train(&samples).symbols, train_with_maps(&samples).symbols);
1197 }
1198 }
1199}