tf_demo_parser/demo/message/
stringtable.rs

1use bitbuffer::{
2    BitError, BitReadBuffer, BitReadStream, BitWrite, BitWriteSized, BitWriteStream, LittleEndian,
3};
4use num_traits::{PrimInt, Unsigned};
5use serde::{Deserialize, Serialize};
6use snap::raw::{decompress_len, Decoder};
7
8use crate::demo::lzss::decompress;
9use crate::demo::packet::stringtable::{
10    ExtraData, FixedUserDataSize, StringTable, StringTableEntry,
11};
12use crate::demo::parser::{Encode, ParseBitSkip};
13use crate::{Parse, ParseError, ParserState, ReadResult, Result, Stream};
14use std::borrow::Cow;
15use std::cmp::min;
16
17#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
18#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
19#[serde(bound(deserialize = "'a: 'static"))]
20pub struct CreateStringTableMessage<'a> {
21    pub table: StringTable<'a>,
22}
23
24#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26pub struct StringTableMeta {
27    pub max_entries: u16,
28    pub fixed_userdata_size: Option<FixedUserDataSize>,
29}
30
31impl From<&StringTable<'_>> for StringTableMeta {
32    fn from(table: &StringTable) -> Self {
33        StringTableMeta {
34            max_entries: table.max_entries,
35            fixed_userdata_size: table.fixed_user_data_size,
36        }
37    }
38}
39
40impl<'a> Parse<'a> for CreateStringTableMessage<'a> {
41    fn parse(stream: &mut Stream<'a>, state: &ParserState) -> Result<Self> {
42        let name = stream.read()?;
43        let max_entries: u16 = stream.read()?;
44        let encode_bits = log_base2(max_entries);
45        let entity_count: u16 = stream.read_sized(encode_bits as usize + 1)?;
46        let length = if state.protocol_version > 23 {
47            read_var_int(stream)?
48        } else {
49            stream.read_sized(20)?
50        };
51
52        let fixed_userdata_size = stream.read()?;
53
54        let compressed = stream.read()?;
55
56        let mut table_data = stream.read_bits(length as usize)?;
57
58        if compressed {
59            let decompressed_size: u32 = table_data.read()?;
60            let compressed_size: u32 = table_data.read()?;
61
62            if !(4..=10 * 1024 * 1024).contains(&compressed_size) {
63                return Err(ParseError::InvalidDemo(
64                    "Invalid compressed string table size",
65                ));
66            }
67
68            if decompressed_size > 100 * 1024 * 1024 {
69                return Err(ParseError::InvalidDemo(
70                    "Invalid decompressed string table size",
71                ));
72            }
73
74            let magic = table_data.read_string(Some(4))?;
75
76            match magic.as_ref() {
77                "SNAP" => {
78                    let compressed_data = table_data.read_bytes(compressed_size as usize - 4)?;
79
80                    let mut decoder = Decoder::new();
81
82                    let decompressed_size_from_header = decompress_len(&compressed_data)?;
83
84                    if decompressed_size_from_header != decompressed_size as usize {
85                        return Err(ParseError::UnexpectedDecompressedSize {
86                            expected: decompressed_size,
87                            size: decompressed_size_from_header as u32,
88                        });
89                    }
90
91                    let mut decompressed_data = vec![0; decompressed_size_from_header];
92                    decoder
93                        .decompress(&compressed_data, &mut decompressed_data)
94                        .map_err(ParseError::from)?;
95
96                    let buffer = BitReadBuffer::new_owned(decompressed_data, LittleEndian);
97                    table_data = BitReadStream::new(buffer);
98                }
99                "LZSS" => {
100                    let compressed_data = table_data.read_bytes(compressed_size as usize - 4)?;
101                    let mut decompressed_data = Vec::with_capacity(decompressed_size as usize);
102                    decompress(&compressed_data, &mut decompressed_data);
103
104                    if decompressed_data.len() != decompressed_size as usize {
105                        return Err(ParseError::UnexpectedDecompressedSize {
106                            expected: decompressed_size,
107                            size: decompressed_data.len() as u32,
108                        });
109                    }
110
111                    let buffer = BitReadBuffer::new_owned(decompressed_data, LittleEndian);
112                    table_data = BitReadStream::new(buffer);
113                }
114                _ => {
115                    return Err(ParseError::UnexpectedCompressionType(magic.into_owned()));
116                }
117            }
118        }
119
120        let table_meta = StringTableMeta {
121            max_entries,
122            fixed_userdata_size,
123        };
124
125        let entries = parse_string_table_update(&mut table_data, &table_meta, entity_count)?;
126
127        let table = StringTable {
128            entries,
129            max_entries,
130            fixed_user_data_size: fixed_userdata_size,
131            client_entries: None,
132            compressed,
133            name,
134        };
135        Ok(CreateStringTableMessage { table })
136    }
137}
138
139impl<'a> ParseBitSkip<'a> for CreateStringTableMessage<'a> {
140    fn parse_skip(stream: &mut Stream<'a>, state: &ParserState) -> Result<()> {
141        let _: String = stream.read()?;
142        let max_entries: u16 = stream.read()?;
143        let encode_bits = log_base2(max_entries);
144        let _: u16 = stream.read_sized(encode_bits as usize + 1)?;
145        let length = if state.protocol_version > 23 {
146            read_var_int(stream)?
147        } else {
148            stream.read_sized(20)?
149        };
150
151        let _: Option<FixedUserDataSize> = stream.read()?;
152
153        let _: bool = stream.read()?;
154
155        stream.skip_bits(length as usize).map_err(ParseError::from)
156    }
157}
158
159impl Encode for CreateStringTableMessage<'_> {
160    fn encode(
161        &self,
162        stream: &mut BitWriteStream<LittleEndian>,
163        _state: &ParserState,
164    ) -> Result<()> {
165        let table = &self.table;
166        table.name.write(stream)?;
167        table.max_entries.write(stream)?;
168        let encode_bits = log_base2(table.max_entries) as usize;
169        (table.entries.len() as u16).write_sized(stream, encode_bits + 1)?;
170
171        stream.reserve_int::<ParseError, _>(40, |stream| {
172            table.fixed_user_data_size.is_some().write(stream)?;
173            if let Some(fixed_size) = table.fixed_user_data_size {
174                fixed_size.write(stream)?;
175            }
176
177            // no compression for now
178            false.write(stream)?;
179
180            let start = stream.bit_len();
181
182            let table_meta = table.get_table_meta();
183
184            write_string_table_update(&table.entries, stream, &table_meta)?;
185
186            let end = stream.bit_len();
187            Ok(encode_var_int_fixed((end - start) as u32))
188        })?;
189
190        Ok(())
191    }
192}
193
194#[test]
195fn test_create_string_table_roundtrip() {
196    let state = ParserState::new(24, |_| false, false);
197    crate::test_roundtrip_encode(
198        CreateStringTableMessage {
199            table: StringTable {
200                name: "table1".into(),
201                entries: vec![],
202                max_entries: 16,
203                fixed_user_data_size: None,
204                client_entries: None,
205                compressed: false,
206            },
207        },
208        &state,
209    );
210    crate::test_roundtrip_encode(
211        CreateStringTableMessage {
212            table: StringTable {
213                name: "table1".into(),
214                entries: vec![
215                    (
216                        0,
217                        StringTableEntry {
218                            text: Some("foo".into()),
219                            extra_data: None,
220                        },
221                    ),
222                    (
223                        1,
224                        StringTableEntry {
225                            text: Some("bar".into()),
226                            extra_data: None,
227                        },
228                    ),
229                ],
230                max_entries: 16,
231                fixed_user_data_size: Some(FixedUserDataSize { size: 12, bits: 4 }),
232                client_entries: None,
233                compressed: false,
234            },
235        },
236        &state,
237    );
238}
239
240#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
241#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
242#[serde(bound(deserialize = "'a: 'static"))]
243pub struct UpdateStringTableMessage<'a> {
244    pub entries: Vec<(u16, StringTableEntry<'a>)>,
245    pub table_id: u8,
246}
247
248impl<'a> Parse<'a> for UpdateStringTableMessage<'a> {
249    fn parse(stream: &mut Stream<'a>, state: &ParserState) -> Result<Self> {
250        let table_id = stream.read_sized(5)?;
251
252        let changed: u16 = if stream.read()? { stream.read()? } else { 1 };
253        let length: u32 = stream.read_int(20)?;
254
255        let mut data = stream.read_bits(length as usize)?;
256
257        let entries = match state.string_tables.get(table_id as usize) {
258            Some(table) => parse_string_table_update(&mut data, table, changed),
259            None => return Err(ParseError::StringTableNotFound(table_id)),
260        }?;
261
262        Ok(UpdateStringTableMessage { entries, table_id })
263    }
264}
265
266impl<'a> ParseBitSkip<'a> for UpdateStringTableMessage<'a> {
267    fn parse_skip(stream: &mut Stream<'a>, _state: &ParserState) -> Result<()> {
268        let _: u8 = stream.read_sized(5)?;
269
270        let _: u16 = if stream.read()? { stream.read()? } else { 1 };
271        let length: u32 = stream.read_int(20)?;
272        stream.skip_bits(length as usize).map_err(ParseError::from)
273    }
274}
275
276impl Encode for UpdateStringTableMessage<'_> {
277    fn encode(&self, stream: &mut BitWriteStream<LittleEndian>, state: &ParserState) -> Result<()> {
278        self.table_id.write_sized(stream, 5)?;
279        if self.entries.len() == 1 {
280            false.write(stream)?;
281        } else {
282            true.write(stream)?;
283            (self.entries.len() as u16).write(stream)?;
284        }
285
286        match state.string_tables.get(self.table_id as usize) {
287            Some(table) => Ok(stream.reserve_length(20, |stream| {
288                write_string_table_update(&self.entries, stream, table)
289            })?),
290            None => Err(ParseError::StringTableNotFound(self.table_id)),
291        }
292    }
293}
294
295#[test]
296fn test_update_string_table_roundtrip() {
297    let mut state = ParserState::new(24, |_| false, false);
298    state.string_tables = vec![StringTableMeta {
299        max_entries: 16,
300        fixed_userdata_size: None,
301    }];
302    crate::test_roundtrip_encode(
303        UpdateStringTableMessage {
304            entries: vec![],
305            table_id: 0,
306        },
307        &state,
308    );
309    crate::test_roundtrip_encode(
310        UpdateStringTableMessage {
311            entries: vec![(
312                2,
313                StringTableEntry {
314                    text: Some("foo".into()),
315                    extra_data: None,
316                },
317            )],
318            table_id: 0,
319        },
320        &state,
321    );
322    crate::test_roundtrip_encode(
323        UpdateStringTableMessage {
324            entries: vec![
325                (
326                    2,
327                    StringTableEntry {
328                        text: Some("foo".into()),
329                        extra_data: None,
330                    },
331                ),
332                (
333                    3,
334                    StringTableEntry {
335                        text: Some("bar".into()),
336                        extra_data: None,
337                    },
338                ),
339            ],
340            table_id: 0,
341        },
342        &state,
343    );
344}
345
346struct TableEntries<'a> {
347    entries: Vec<(u16, StringTableEntry<'a>)>,
348    history: Vec<u16>,
349}
350
351impl<'a> TableEntries<'a> {
352    pub fn new(count: usize) -> Self {
353        TableEntries {
354            entries: Vec::with_capacity(min(count, 128)),
355            history: Vec::with_capacity(32),
356        }
357    }
358
359    pub fn push(&mut self, entry: (u16, StringTableEntry<'a>)) {
360        if self.history.len() > 31 {
361            self.history.remove(0);
362        }
363        let entry_index = self.entries.len();
364        self.entries.push(entry);
365        self.history.push(entry_index as u16);
366    }
367
368    pub fn get_history(&self, index: usize) -> Option<&StringTableEntry<'a>> {
369        self.history
370            .get(index)
371            .and_then(|entry_index| self.entries.get(*entry_index as usize))
372            .map(|entry| &entry.1)
373    }
374
375    pub fn into_entries(self) -> Vec<(u16, StringTableEntry<'a>)> {
376        self.entries
377    }
378
379    pub fn find_best_history(&self, text: &str) -> Option<(usize, usize)> {
380        let mut best_index = None;
381        let mut best_count = 0;
382        for (history_index, entry_index) in self.history.iter().enumerate() {
383            if let Some((_, entry)) = self.entries.get(*entry_index as usize) {
384                let similar = min(31, count_similar_characters(entry.text(), text));
385                if similar >= 3 && similar > best_count {
386                    best_index = Some(history_index);
387                    best_count = similar;
388                }
389            }
390        }
391
392        best_index.map(|index| (index, best_count))
393    }
394}
395
396fn count_similar_characters(a: &str, b: &str) -> usize {
397    for (i, (a, b)) in a.bytes().zip(b.bytes()).enumerate() {
398        if a != b {
399            return i;
400        }
401    }
402    min(a.len(), b.len())
403}
404
405pub fn parse_string_table_update<'a>(
406    stream: &mut Stream<'a>,
407    table_meta: &StringTableMeta,
408    entry_count: u16,
409) -> ReadResult<Vec<(u16, StringTableEntry<'a>)>> {
410    let entry_bits = log_base2(table_meta.max_entries);
411    let mut entries = TableEntries::new(entry_count as usize);
412
413    let mut last_entry: i16 = -1;
414
415    for _ in 0..entry_count {
416        let index = if stream.read()? {
417            last_entry.saturating_add(1) as u16
418        } else {
419            stream.read_sized(entry_bits as usize)?
420        };
421
422        last_entry = index as i16;
423
424        let entry = read_table_entry(stream, table_meta, &entries)?;
425        entries.push((index, entry));
426    }
427
428    Ok(entries.into_entries())
429}
430
431pub fn write_string_table_update(
432    entries: &[(u16, StringTableEntry)],
433    stream: &mut BitWriteStream<LittleEndian>,
434    table_meta: &StringTableMeta,
435) -> ReadResult<()> {
436    let entry_bits = log_base2(table_meta.max_entries);
437
438    let mut last_entry: i16 = -1;
439    let mut history = TableEntries::new(entries.len());
440
441    for (index, entry) in entries.iter() {
442        let index = *index as i16;
443        if index == (last_entry + 1) {
444            true.write(stream)?;
445        } else {
446            false.write(stream)?;
447            index.write_sized(stream, entry_bits as usize)?;
448        }
449        last_entry = index;
450
451        write_table_entry(entry, stream, table_meta, &history)?;
452        history.push((index as u16, entry.clone()));
453    }
454
455    Ok(())
456}
457
458#[test]
459fn test_table_update_roundtrip() {
460    fn entry_roundtrip(
461        entries: Vec<(u16, StringTableEntry)>,
462        max_entries: u16,
463        fixed_bits: Option<u8>,
464    ) {
465        let table_meta = StringTableMeta {
466            max_entries,
467            fixed_userdata_size: fixed_bits.map(|bits| FixedUserDataSize { size: 0, bits }),
468        };
469        let mut data = Vec::new();
470        let pos = {
471            let mut write = BitWriteStream::new(&mut data, LittleEndian);
472            write_string_table_update(&entries, &mut write, &table_meta).unwrap();
473            write.bit_len()
474        };
475        let mut read = BitReadStream::new(BitReadBuffer::new(&data, LittleEndian));
476        assert_eq!(
477            entries,
478            parse_string_table_update(&mut read, &table_meta, entries.len() as u16).unwrap()
479        );
480        assert_eq!(pos, read.pos());
481    }
482    entry_roundtrip(
483        vec![(
484            3,
485            StringTableEntry {
486                text: None,
487                extra_data: None,
488            },
489        )],
490        8,
491        None,
492    );
493    entry_roundtrip(
494        vec![
495            (
496                0,
497                StringTableEntry {
498                    text: Some("bar".into()),
499                    extra_data: None,
500                },
501            ),
502            (
503                1,
504                StringTableEntry {
505                    text: Some("foo".into()),
506                    extra_data: None,
507                },
508            ),
509            (
510                5,
511                StringTableEntry {
512                    text: Some("asd".into()),
513                    extra_data: None,
514                },
515            ),
516        ],
517        16,
518        None,
519    );
520    entry_roundtrip(
521        vec![
522            (
523                1,
524                StringTableEntry {
525                    text: Some("foo".into()),
526                    extra_data: None,
527                },
528            ),
529            (
530                2,
531                StringTableEntry {
532                    text: Some("asd".into()),
533                    extra_data: None,
534                },
535            ),
536        ],
537        16,
538        None,
539    );
540    entry_roundtrip(
541        vec![(
542            1,
543            StringTableEntry {
544                text: Some("foo".into()),
545                extra_data: None,
546            },
547        )],
548        16,
549        None,
550    );
551}
552
553fn read_table_entry<'a>(
554    stream: &mut Stream<'a>,
555    table_meta: &StringTableMeta,
556    history: &TableEntries,
557) -> ReadResult<StringTableEntry<'a>> {
558    let text = if stream.read()? {
559        // set value
560        if stream.read()? {
561            // reuse from history
562            let index: u16 = stream.read_sized(5)?;
563            let bytes_to_copy: u32 = stream.read_sized(5)?;
564            let rest_of_string: Cow<str> = stream.read()?;
565
566            Some(
567                match history
568                    .get_history(index as usize)
569                    .and_then(|entry| entry.text.as_ref())
570                {
571                    Some(text) => Cow::Owned(String::from_utf8({
572                        text.bytes()
573                            .take(bytes_to_copy as usize)
574                            .chain(rest_of_string.bytes())
575                            .collect()
576                    })?),
577                    None => rest_of_string, // best guess, happens in some pov demos but only for unimportant tables it seems
578                },
579            )
580        } else {
581            Some(stream.read()?)
582        }
583    } else {
584        None
585    };
586
587    let extra_data = if stream.read()? {
588        Some(match table_meta.fixed_userdata_size {
589            Some(size) => stream.read_bits(size.bits as usize)?,
590            None => {
591                let bytes: u16 = stream.read_sized(14)?;
592                stream.read_bits(bytes as usize * 8)?
593            }
594        })
595    } else {
596        None
597    }
598    .map(ExtraData::new);
599
600    Ok(StringTableEntry { text, extra_data })
601}
602
603fn write_table_entry(
604    entry: &StringTableEntry,
605    stream: &mut BitWriteStream<LittleEndian>,
606    table_meta: &StringTableMeta,
607    history: &TableEntries,
608) -> ReadResult<()> {
609    entry.text.is_some().write(stream)?;
610    if let Some(text) = entry.text.as_deref() {
611        let history_item = history.find_best_history(text);
612        history_item.is_some().write(stream)?;
613        if let Some((history_index, history_count)) = history_item {
614            history_index.write_sized(stream, 5)?;
615            history_count.write_sized(stream, 5)?;
616            let diff_bytes =
617                text.as_bytes()
618                    .get(history_count..)
619                    .ok_or(BitError::IndexOutOfBounds {
620                        pos: history_count,
621                        size: text.len(),
622                    })?;
623            stream.write_bytes(diff_bytes)?;
624            0u8.write(stream)?; // writing the string as bytes doesn't add the null terminator
625        } else {
626            text.write(stream)?;
627        }
628    }
629
630    entry.extra_data.is_some().write(stream)?;
631    if let Some(extra_data) = entry.extra_data.as_ref() {
632        match table_meta.fixed_userdata_size {
633            Some(size) => {
634                extra_data.data.write_sized(stream, size.bits as usize)?;
635            }
636            None => {
637                extra_data.byte_len.write_sized(stream, 14)?;
638                extra_data
639                    .data
640                    .write_sized(stream, extra_data.byte_len as usize * 8)?;
641            }
642        }
643    }
644
645    Ok(())
646}
647
648#[test]
649fn test_table_entry_roundtrip() {
650    fn entry_roundtrip(entry: StringTableEntry, fixed_bits: Option<u8>) {
651        let table_meta = StringTableMeta {
652            max_entries: 0,
653            fixed_userdata_size: fixed_bits.map(|bits| FixedUserDataSize { size: 0, bits }),
654        };
655        let mut data = Vec::new();
656        let pos = {
657            let history = TableEntries::new(1);
658            let mut write = BitWriteStream::new(&mut data, LittleEndian);
659            write_table_entry(&entry, &mut write, &table_meta, &history).unwrap();
660            write.bit_len()
661        };
662        let mut read = BitReadStream::new(BitReadBuffer::new(&data, LittleEndian));
663        assert_eq!(
664            entry,
665            read_table_entry(&mut read, &table_meta, &TableEntries::new(0)).unwrap()
666        );
667        assert_eq!(pos, read.pos());
668    }
669    entry_roundtrip(
670        StringTableEntry {
671            text: None,
672            extra_data: None,
673        },
674        None,
675    );
676    entry_roundtrip(
677        StringTableEntry {
678            text: Some("foo".into()),
679            extra_data: None,
680        },
681        None,
682    );
683    entry_roundtrip(
684        StringTableEntry {
685            text: None,
686            extra_data: Some(ExtraData::new(BitReadStream::new(
687                BitReadBuffer::new_owned(vec![0x55], LittleEndian),
688            ))),
689        },
690        None,
691    );
692    entry_roundtrip(
693        StringTableEntry {
694            text: None,
695            extra_data: Some(ExtraData::new(BitReadStream::new(
696                BitReadBuffer::new_owned(vec![0x55; 128], LittleEndian),
697            ))),
698        },
699        None,
700    );
701    entry_roundtrip(
702        StringTableEntry {
703            text: None,
704            extra_data: Some(ExtraData::new(BitReadStream::new(
705                BitReadBuffer::new_owned(vec![0x55; 4], LittleEndian),
706            ))),
707        },
708        Some(4 * 8),
709    );
710}
711
712pub fn read_var_int(stream: &mut Stream) -> ReadResult<u32> {
713    let mut result: u32 = 0;
714    for i in (0..35u32).step_by(7) {
715        let byte: u8 = stream.read()?;
716        result |= (byte as u32 & 0x7F) << i;
717
718        if (byte >> 7) == 0 {
719            break;
720        }
721    }
722    Ok(result)
723}
724
725pub fn write_var_int(mut int: u32, stream: &mut BitWriteStream<LittleEndian>) -> ReadResult<()> {
726    while int > 0x7F {
727        let byte: u8 = int as u8 & 0x7F;
728        (byte | 0x80).write(stream)?;
729        int >>= 7;
730    }
731    (int as u8).write(stream)
732}
733
734// encode the int in such a way that it has a fixed size, but still decodes the same
735// result is the first 40 bits of the return value
736pub fn encode_var_int_fixed(mut int: u32) -> u64 {
737    let mut out = 0;
738    for i in 0..4 {
739        let byte: u8 = int as u8 & 0x7F;
740        out |= ((byte | 0x80) as u64) << (i * 8);
741        int >>= 7;
742    }
743    out |= (int as u64) << 32;
744    out
745}
746
747#[test]
748fn test_var_int_roundtrip() {
749    fn var_int_roundtrip(int: u32) {
750        let mut data = Vec::new();
751        let pos = {
752            let mut write = BitWriteStream::new(&mut data, LittleEndian);
753            write_var_int(int, &mut write).unwrap();
754            write.bit_len()
755        };
756        let mut read = BitReadStream::new(BitReadBuffer::new(&data, LittleEndian));
757        assert_eq!(int, read_var_int(&mut read).unwrap());
758        assert_eq!(pos, read.pos());
759    }
760    var_int_roundtrip(0);
761    var_int_roundtrip(1);
762    var_int_roundtrip(10);
763    var_int_roundtrip(55);
764    var_int_roundtrip(355);
765    var_int_roundtrip(12354);
766    var_int_roundtrip(123125412);
767}
768
769#[test]
770fn test_var_int_fixed_roundtrip() {
771    fn var_int_roundtrip(int: u32) {
772        let mut data = Vec::new();
773        let pos = {
774            let mut write = BitWriteStream::new(&mut data, LittleEndian);
775            let encoded = encode_var_int_fixed(int);
776            encoded.write_sized(&mut write, 40).unwrap();
777            write.bit_len()
778        };
779        assert_eq!(40, pos);
780        let mut read = BitReadStream::new(BitReadBuffer::new(&data, LittleEndian));
781        assert_eq!(int, read_var_int(&mut read).unwrap());
782        assert_eq!(pos, read.pos());
783    }
784    var_int_roundtrip(0);
785    var_int_roundtrip(1);
786    var_int_roundtrip(10);
787    var_int_roundtrip(55);
788    var_int_roundtrip(355);
789    var_int_roundtrip(12354);
790    var_int_roundtrip(123125412);
791}
792
793pub fn log_base2<T: PrimInt + Unsigned>(num: T) -> u32 {
794    // log(0) = inf, but that's a useless result
795    // since this would only happen in malformed demos, we just return 0
796    (std::mem::size_of::<T>() as u32 * 8 - 1).saturating_sub(num.leading_zeros())
797}