teehistorian 0.12.0

Teehistorian parser for DDNet
Documentation
//! teehistorian parser for DDNet
//!
//! This crate provides a library for safe parsing of [teehistorian](https://ddnet.tw/docs/libtw2/teehistorian)
//! files from the game [DDNet](https://ddnet.tw). Goals of this library are:
//!
//! - performance
//!   - the header is retrievable without loading the whole file
//!   - the teehistorian file doesn't have to fit into memory
//! - provide a C-API to eventually integrate it into [DDNet](https://github.com/ddnet/ddnet) for a
//!   teehistorian replayer
//!
//! In the very center of this library is the [Th](Th) struct to get the stream of [Chunks](Chunk) of
//! an teehistorian file
//!
//! # Examples
//!
//! With the teehistorian file loaded directly into memory
//!
//! ```rust
//! use teehistorian::{Chunk, Th};
//! let input = b"\x69\x9d\xb1\x7b\x8e\xfb\x34\xff\xb1\xd8\xda\x6f\x60\xc1\x5d\xd1\
//!            {\"version\":\"2\"}\x00\
//!            \x40";
//! let mut th = Th::parse(&input[..]).unwrap();
//! assert_eq!(th.header().unwrap(), br#"{"version":"2"}"#);
//! assert_eq!(th.next_chunk().unwrap(), Chunk::Eos);
//! assert!(th.next_chunk().unwrap_err().is_eof());
//! ```
//!
//! When operating with files:
//!
//! ```rust
//! use teehistorian::{Chunk, ThBufReader, Th};
//! use std::fs::File;
//!
//! let f = File::open("tests/minimal.teehistorian").unwrap();
//! let mut th = Th::parse(ThBufReader::new(f)).unwrap();
//! assert_eq!(th.header().unwrap(), br#"{"version":"2"}"#);
//! assert_eq!(th.next_chunk().unwrap(), Chunk::Eos);
//! assert!(th.next_chunk().unwrap_err().is_eof());
//! ```
//!

pub mod chunks;
mod compat;
mod error;
mod parser;
mod ser;

pub use bufread::{ThBufRead, ThBufReader};
pub use chunks::Chunk;
pub use compat::{ThCompat, ThStream};
pub use error::{Error, ErrorKind};
pub use ser::ThWriter;

/// export packer for now. TODO: move to separate crate
pub use ser::serialize_into;

mod bufread;

use crate::parser::cstring;
use uuid::Uuid;

/// Straight forward Teehistorian reader: outputs the chunks as they are in the file.
/// With this reader, you need to be careful with Teehistorian version differences.
/// For a more consistent reader, which takes care of some differences, use [`ThCompat`].
#[derive(Debug)]
pub struct Th<T: ThBufRead> {
    buf: T,
    parsed_header: bool,
    parsed_chunk: bool,
    num_consume: usize,
}

impl<T: ThBufRead> Th<T> {
    pub fn parse(inp: T) -> Result<Self, crate::error::Error> {
        let mut inp = inp;
        if inp.fill_buf()? == 0 && inp.get_buf().is_empty() {
            return Err(crate::error::ErrorKind::NotTeehistorian(None).into());
        }
        loop {
            let buf = inp.get_buf();
            if buf.len() >= 16 {
                // can unwrap since already checked, that 16 bytes are in the buffer to parse the uuid
                let (input, uuid) = parser::uuid(buf).unwrap();
                if uuid != Uuid::from_u128(0x699db17b_8efb_34ff_b1d8_da6f60c15dd1) {
                    return Err(ErrorKind::NotTeehistorian(Some(uuid)).into());
                }
                // safe since computing difference to subslice
                let num_consumed = unsafe { input.as_ptr().offset_from(buf.as_ptr()) };
                inp.consume(num_consumed as usize);
                break;
            }
            if inp.fill_buf()? == 0 {
                return Err(crate::error::ErrorKind::NotTeehistorian(None).into());
            }
        }
        Ok(Th {
            buf: inp,
            parsed_header: false,
            parsed_chunk: false,
            num_consume: 0,
        })
    }

    /// Tries to parse the teehistorian header returns a byte slice to a json object excluding without
    /// the null terminator present in the teehistorian format
    ///
    /// # Panic
    ///
    /// `header()` must only be called before the first chunk is requested with [next_chunk()](Th.next_chunk)
    /// Otherwise the functions panics.
    pub fn header(&mut self) -> Result<&[u8], crate::error::Error> {
        if self.parsed_chunk {
            panic!("Teehistorian library misuse. Accessing the header is only allowed before accessing the first chunk");
        }
        self.parsed_header = true;
        loop {
            let buf = self.buf.get_buf();
            // FIXME: when not necessary from non-lexical lifetimes
            let buf = unsafe { std::slice::from_raw_parts(buf.as_ptr(), buf.len()) };
            match cstring(buf) {
                Ok((new_input, header)) => {
                    // safe since computing difference to subslice
                    self.num_consume =
                        unsafe { new_input.as_ptr().offset_from(buf.as_ptr()) } as usize;
                    return Ok(header);
                }
                Err(nom::Err::Error(err)) | Err(nom::Err::Failure(err)) => {
                    self.num_consume = 0;
                    return Err(err.into());
                }
                Err(nom::Err::Incomplete(_)) => {
                    if self.buf.fill_buf()? == 0 {
                        // End of file
                        return Err(crate::error::Error::Eof);
                    }
                }
            }
        }
    }

    pub fn next_chunk(&mut self) -> Result<chunks::Chunk, crate::error::Error> {
        if !self.parsed_header {
            self.header()?;
        }
        self.parsed_chunk = true;
        // consume data from last chunk, since it isn't referenced anymore
        if self.num_consume != 0 {
            self.buf.consume(self.num_consume);
            self.num_consume = 0;
        }

        loop {
            let buf = self.buf.get_buf();
            // FIXME: when not necessary from non-lexical lifetimes
            let buf = unsafe { std::slice::from_raw_parts(buf.as_ptr(), buf.len()) };
            match chunks::chunk(buf) {
                Ok((new_input, chunk)) => {
                    // safe since computing difference to subslice
                    self.num_consume =
                        unsafe { new_input.as_ptr().offset_from(buf.as_ptr()) } as usize;
                    return Ok(chunk);
                }
                Err(nom::Err::Error(err)) | Err(nom::Err::Failure(err)) => {
                    self.num_consume = 0;
                    return Err(err.into());
                }
                Err(nom::Err::Incomplete(_)) => {
                    if self.buf.fill_buf()? == 0 {
                        // End of file
                        return Err(crate::error::Error::Eof);
                    }
                }
            }
        }
    }

    /// reset parser to before last `next_chunk` call. Can be only called once after each
    /// `next_chunk` call. The next `next_chunk` call will return the same as the previous one.
    pub fn reset_chunk(&mut self) {
        // number of bytes in previous chunk must be greater than 0
        assert!(self.num_consume > 0);
        self.num_consume = 0;
    }

    /// Extract the buffer to reuse in a new teehistorian file
    pub fn reuse_buffer(self) -> T {
        self.buf
    }
}