#[repr(packed)] #[derive(Debug, Clone, Copy)] struct Header { secs: u32, micros: u32, len: u32, } impl Header { fn time(&self) -> std::time::Duration { std::time::Duration::from_micros( u64::from(self.secs) * 1_000_000 + u64::from(self.micros), ) } fn len(&self) -> usize { usize::try_from(self.len).unwrap_or_else(|_| { panic!("this library requires sizeof(usize) to be at least 4") }) } } /// Parses ttyrec streams. /// /// Designed to be able to be used in a streaming/asynchronous fashion. As you /// read bytes from the ttyrec stream (whether from a file or whatever else), /// call the [`add_bytes`](Parser::add_bytes) method to add them to the /// internal buffer. At any point, you can call /// [`next_frame`](Parser::next_frame) to then return the next complete frame /// if one has been read. #[derive(Debug, Default, Clone)] pub struct Parser { reading: std::collections::VecDeque, read_state: Option
, offset: Option, } impl Parser { /// Create a new [`Parser`](Self). #[must_use] pub fn new() -> Self { Self::default() } /// Add more bytes to the internal buffer. pub fn add_bytes(&mut self, bytes: &[u8]) { self.reading.extend(bytes.iter()); } /// Try to read a frame from the internal buffer. /// /// If a complete frame is found, the bytes for that frame will be removed /// from the internal buffer and the frame object will be returned. If a /// complete frame is not found, this method will return [`None`]. // these unwraps aren't reachable #[allow(clippy::missing_panics_doc)] pub fn next_frame(&mut self) -> Option { let header = if let Some(header) = &self.read_state { header } else { if self.reading.len() < std::mem::size_of::
() { return None; } // these unwraps are guaranteed safe by the length check above let secs1 = self.reading.pop_front().unwrap(); let secs2 = self.reading.pop_front().unwrap(); let secs3 = self.reading.pop_front().unwrap(); let secs4 = self.reading.pop_front().unwrap(); let secs = u32::from_le_bytes([secs1, secs2, secs3, secs4]); let micros1 = self.reading.pop_front().unwrap(); let micros2 = self.reading.pop_front().unwrap(); let micros3 = self.reading.pop_front().unwrap(); let micros4 = self.reading.pop_front().unwrap(); let micros = u32::from_le_bytes([micros1, micros2, micros3, micros4]); let len1 = self.reading.pop_front().unwrap(); let len2 = self.reading.pop_front().unwrap(); let len3 = self.reading.pop_front().unwrap(); let len4 = self.reading.pop_front().unwrap(); let len = u32::from_le_bytes([len1, len2, len3, len4]); let header = Header { secs, micros, len }; self.read_state = Some(header); // unwrap is safe because we just set self.read_state to Some self.read_state.as_ref().unwrap() }; if self.reading.len() < header.len() { return None; } let mut data = vec![]; for _ in 0..header.len() { data.push( // unwrap is safe because we just checked that there are // sufficient bytes in self.reading self.reading.pop_front().unwrap(), ); } let time = header.time(); self.read_state = None; if self.offset.is_none() { self.offset = Some(time); } Some(crate::frame::Frame { time, data }) } /// How much the timestamps in this file should be offset by. /// /// Ttyrec files are allowed to be generated by just inserting the current /// absolute timestamp as the header. This means that during playback, we /// need to take the timestamp of the first frame as the start time, and /// each frame timestamp after that should be offset by that same amount. /// /// Returns [`None`] if no frames have been read yet. #[must_use] pub fn offset(&self) -> Option { self.offset } } #[cfg(test)] mod test { use super::*; #[test] fn test_basic() { let bytes = vec![ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 38, 0, 0, 0, 64, 226, 1, 0, 10, 0, 0, 0, 27, 91, 50, 74, 102, 111, 111, 98, 97, 114, ]; let mut parser = Parser::new(); for (i, c) in bytes.into_iter().enumerate() { parser.add_bytes(&[c]); let expected = match i { 11 => { let time = std::time::Duration::new(0, 0); let data = b"".to_vec(); Some(crate::frame::Frame { time, data }) } 33 => { let time = std::time::Duration::new(38, 123_456_000); let data = b"\x1b[2Jfoobar".to_vec(); Some(crate::frame::Frame { time, data }) } _ => None, }; let got = parser.next_frame(); assert_eq!(got, expected); } } }