diff --git a/Cargo.toml b/Cargo.toml index 4cc66ef..5cad1e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,5 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +flate2 = { version = "1.1.5", features = ["zlib-rs"] } rmp = "0.8.14" thiserror = "2.0.17" +zstd = "0.13.3" diff --git a/src/bin/dump.rs b/src/bin/dump.rs new file mode 100644 index 0000000..bb139aa --- /dev/null +++ b/src/bin/dump.rs @@ -0,0 +1,20 @@ +use std::io::Seek; + +use rply_codec::*; + +pub fn main() { + let args: Vec<_> = std::env::args().collect(); + let file = + std::fs::File::open(args.get(1).unwrap_or(&"examples/bobl.replay".to_string())).unwrap(); + let mut file = std::io::BufReader::new(file); + let header = read_header(&mut file).unwrap(); + println!("{:?}", header); + let initial_size = match &header { + Header::V0V1(header_base) => header_base.initial_state_size, + Header::V2(header_v2) => header_v2.base.initial_state_size, + }; + file.seek_relative(initial_size as i64).unwrap(); + let mut frame = Frame::default(); + read_frame(&mut file, &header, &mut frame).unwrap(); + println!("{:?}", frame); +} diff --git a/src/lib.rs b/src/lib.rs index 78e1d0a..268b7e2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,5 @@ mod rply; - -pub fn add(left: u64, right: u64) -> u64 { - left + right -} +pub use rply::*; #[cfg(test)] mod tests { @@ -10,17 +7,20 @@ mod tests { #[test] fn v2_header() { - let mut file = std::fs::File::open("examples/bobl.replay").unwrap(); + let mut file = std::io::BufReader::new(std::fs::File::open("examples/bobl.replay").unwrap()); let header = match rply::read_header(&mut file).unwrap() { rply::Header::V0V1(_) => panic!("Version too low"), rply::Header::V2(h) => h, }; - // content_crc: 2199475946, - // initial_state_size: 2531, - // identifier: 1761326589, assert_eq!(header.base.version, 2); assert_eq!(header.base.content_crc, 2199475946); assert_eq!(header.base.initial_state_size, 2531); assert_eq!(header.base.identifier, 1761326589); + assert_eq!(header.frame_count, 6383); + assert_eq!(header.block_size, 128); + assert_eq!(header.superblock_size, 16); + assert_eq!(header.checkpoint_commit_interval, 4); + assert_eq!(header.checkpoint_commit_threshold, 2); + assert_eq!(header.checkpoint_compression, rply::Compression::None); } } diff --git a/src/rply.rs b/src/rply.rs index ab193f7..6c9cfd1 100644 --- a/src/rply.rs +++ b/src/rply.rs @@ -1,3 +1,13 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub struct InvalidDeterminant(pub u8); +impl std::fmt::Display for InvalidDeterminant { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + #[repr(usize)] pub enum HeaderV0V1Part { Magic = 0, @@ -9,11 +19,6 @@ pub enum HeaderV0V1Part { } #[repr(usize)] enum HeaderV2Part { - Magic = 0, - Version = 4, - CRC = 8, - StateSize = 12, - Identifier = 16, FrameCount = 24, BlockSize = 28, SuperblockSize = 32, @@ -35,6 +40,16 @@ pub enum FrameToken { Checkpoint = b'c', Checkpoint2 = b'C', } +impl From for FrameToken { + fn from(value: u8) -> Self { + match value { + b'f' => FrameToken::Regular, + b'c' => FrameToken::Checkpoint, + b'C' => FrameToken::Checkpoint2, + _ => FrameToken::Invalid, + } + } +} #[repr(u8)] #[non_exhaustive] @@ -45,27 +60,61 @@ pub enum SSToken { NewSuperblock = 2, SuperblockSeq = 3, } +impl TryFrom for SSToken { + type Error = InvalidDeterminant; + + fn try_from(value: u8) -> std::result::Result { + match value { + 0 => Ok(SSToken::Start), + 1 => Ok(SSToken::NewBlock), + 2 => Ok(SSToken::NewSuperblock), + 3 => Ok(SSToken::SuperblockSeq), + _ => Err(InvalidDeterminant(value)), + } + } +} #[repr(u8)] #[non_exhaustive] -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum Compression { None = 0, Zlib = 1, Zstd = 2, } -impl TryFrom for Compression {} +impl TryFrom for Compression { + type Error = InvalidDeterminant; + + fn try_from(value: u8) -> std::result::Result { + match value { + 0 => Ok(Compression::None), + 1 => Ok(Compression::Zlib), + 2 => Ok(Compression::Zstd), + _ => Err(InvalidDeterminant(value)), + } + } +} #[repr(u8)] #[non_exhaustive] -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum Encoding { Raw = 0, Statestream = 1, } -impl TryFrom for Encoding {} +impl TryFrom for Encoding { + type Error = InvalidDeterminant; + + fn try_from(value: u8) -> std::result::Result { + match value { + 0 => Ok(Encoding::Raw), + 1 => Ok(Encoding::Statestream), + _ => Err(InvalidDeterminant(value)), + } + } +} #[derive(Debug)] pub struct HeaderBase { @@ -92,8 +141,6 @@ pub enum Header { V2(HeaderV2), } -use thiserror::Error; - #[derive(Error, Debug)] pub enum ReplayError { #[error("Invalid replay magic {0}")] @@ -101,15 +148,19 @@ pub enum ReplayError { #[error("Unsupported version {0}")] Version(u32), #[error("Unsupported compression scheme {0}")] - Compression(u8), + Compression(#[from] InvalidDeterminant), #[error("I/O Error")] IO(#[from] std::io::Error), + #[error("Coreless frame read for version 0 not possible")] + NoCoreRead(), + #[error("Invalid frame token {0}")] + BadFrameToken(u8), } type Result = std::result::Result; -pub fn read_header(rply: &mut impl std::io::Read) -> Result
{ - let mut bytes = vec![0; HEADER_LEN_BYTES]; +pub fn read_header(rply: &mut impl std::io::BufRead) -> Result
{ + let mut bytes = [0; HEADER_LEN_BYTES]; rply.read_exact(&mut bytes[0..HEADER_V0V1_LEN_BYTES])?; // These unwraps are safe because if I can take e.g. a slice of length 4, I already have a 4-byte value. // And I know I can take those slices because read_exact read exactly 24 bytes. @@ -159,7 +210,7 @@ pub fn read_header(rply: &mut impl std::io::Read) -> Result
{ if version < 2 { return Ok(Header::V0V1(base)); } - rply.read_exact(&mut bytes[HEADER_V0V1_LEN_BYTES..HEADER_LEN_BYTES]); + rply.read_exact(&mut bytes[HEADER_V0V1_LEN_BYTES..HEADER_LEN_BYTES])?; let frame_count = u32::from_le_bytes( <[u8; 4]>::try_from( &bytes[(HeaderV2Part::FrameCount as usize)..(HeaderV2Part::FrameCount as usize + 4)], @@ -199,3 +250,180 @@ pub fn read_header(rply: &mut impl std::io::Read) -> Result
{ checkpoint_compression, })) } +impl Header { + pub fn version(&self) -> u32 { + match self { + Header::V0V1(header_base) => header_base.version, + Header::V2(header_v2) => header_v2.base.version, + } + } +} +#[derive(Debug, Default)] +pub struct KeyData { + pub down: u8, + pub modf: u16, + pub code: u32, + pub chr: u32, +} +#[derive(Debug, Default)] +pub struct InputData { + pub port: u8, + pub device: u8, + pub idx: u8, + pub id: u16, + pub val: i16, +} + +#[derive(Debug)] +pub struct Frame { + pub key_events: Vec, + pub input_events: Vec, + checkpoint_raw_bytes: Vec, + checkpoint_uncompressed_raw_bytes: Vec, + checkpoint_uncompressed_unencoded_bytes: Vec, + pub checkpoint_compression: Compression, + pub checkpoint_encoding: Encoding, +} + +impl Frame { + pub fn checkpoint_decompressed_data(&self) -> Option<&[u8]> { + if self.checkpoint_raw_bytes.is_empty() { + return None; + } + Some(match self.checkpoint_compression { + Compression::None => self.checkpoint_raw_bytes.as_slice(), + _ => self.checkpoint_uncompressed_raw_bytes.as_slice(), + }) + } + pub fn checkpoint_data(&self) -> Option<&[u8]> { + if self.checkpoint_raw_bytes.is_empty() { + return None; + } + Some(match (self.checkpoint_compression, self.checkpoint_encoding) { + (Compression::None, Encoding::Raw) => self.checkpoint_raw_bytes.as_slice(), + (_, Encoding::Raw) => self.checkpoint_uncompressed_raw_bytes.as_slice(), + (_, _) => self.checkpoint_uncompressed_unencoded_bytes.as_slice() + }) + } +} + +impl Default for Frame { + fn default() -> Self { + Self { + key_events: Default::default(), + input_events: Default::default(), + checkpoint_raw_bytes: Default::default(), + checkpoint_uncompressed_raw_bytes: Default::default(), + checkpoint_uncompressed_unencoded_bytes: Default::default(), + checkpoint_compression: Compression::None, + checkpoint_encoding: Encoding::Raw, + } + } +} +/* TODO instead of Header use ReplayContext and have it include the statestream indices if needed */ +pub fn read_frame(rply: &mut impl std::io::BufRead, header: &Header, frame: &mut Frame) -> Result<()> { + let vsn = header.version(); + if vsn == 0 { + return Err(ReplayError::NoCoreRead()); + } + let mut buf: [u8; 16] = [0; 16]; + if vsn > 1 { + /* skip over the backref */ + rply.read_exact(&mut buf)?; + } + rply.read_exact(&mut buf[0..1])?; + let key_count = buf[0] as usize; + frame.key_events.resize_with(key_count, Default::default); + for ki in 0..key_count { + rply.read_exact(&mut buf[0..12])?; + /* + down, padding, mod_x2, code_x4, char_x4 + */ + let key_data = KeyData { + down: buf[0], + /* buf[1] is padding */ + modf: u16::from_le_bytes(<[u8; 2]>::try_from(&buf[2..4]).unwrap()), + code: u32::from_le_bytes(<[u8; 4]>::try_from(&buf[4..8]).unwrap()), + chr: u32::from_le_bytes(<[u8; 4]>::try_from(&buf[8..12]).unwrap()), + }; + frame.key_events[ki] = key_data; + } + let input_count = u16::from_le_bytes(<[u8; 2]>::try_from(&buf[0..2]).unwrap()) as usize; + frame + .input_events + .resize_with(input_count, Default::default); + for ii in 0..input_count { + rply.read_exact(&mut buf[0..8])?; + /* port, device, idx, padding, id_x2, value_x2 */ + let inp_data = InputData { + port: buf[0], + device: buf[1], + idx: buf[2], + /* buf[3] is padding */ + id: u16::from_le_bytes(<[u8; 2]>::try_from(&buf[4..6]).unwrap()), + val: i16::from_le_bytes(<[u8; 2]>::try_from(&buf[6..8]).unwrap()), + }; + frame.input_events[ii] = inp_data; + } + rply.read_exact(&mut buf[0..1])?; + match FrameToken::from(buf[0]) { + FrameToken::Invalid => return Err(ReplayError::BadFrameToken(buf[0])), + FrameToken::Regular => { + frame.checkpoint_compression = Compression::None; + frame.checkpoint_encoding = Encoding::Raw; + frame.checkpoint_raw_bytes.clear(); + frame.checkpoint_uncompressed_raw_bytes.clear(); + frame.checkpoint_uncompressed_unencoded_bytes.clear(); + } + FrameToken::Checkpoint => { + frame.checkpoint_compression = Compression::None; + frame.checkpoint_encoding = Encoding::Raw; + rply.read_exact(&mut buf[0..8])?; + let cp_size = usize::try_from(u64::from_le_bytes(<[u8; 8]>::try_from(&buf[0..8]).unwrap())).unwrap(); + frame.checkpoint_raw_bytes.resize(cp_size, 0); + rply.read_exact(frame.checkpoint_raw_bytes.as_mut_slice())?; + } + FrameToken::Checkpoint2 => { + rply.read_exact(&mut buf[0..14])?; + // read a 1 byte compression + let compression = Compression::try_from(buf[0])?; + // read a 1 byte encoding + let encoding = Encoding::try_from(buf[1])?; + // read a 4 byte uncompressed unencoded size + let uc_ue_size = u32::from_le_bytes(<[u8; 4]>::try_from(&buf[2..6]).unwrap()) as usize; + // read a 4 byte uncompressed encoded size + let uc_enc_size = u32::from_le_bytes(<[u8; 4]>::try_from(&buf[6..10]).unwrap()) as usize; + // read a 4 byte compressed encoded size + let comp_enc_size = u32::from_le_bytes(<[u8; 4]>::try_from(&buf[10..14]).unwrap()) as usize; + // read the compressed encoded data + frame.checkpoint_raw_bytes.resize(comp_enc_size, 0); + rply.read_exact(frame.checkpoint_raw_bytes.as_mut_slice())?; + // maybe decompress + match compression { + Compression::None => {}, + Compression::Zlib => { + use flate2::bufread::ZlibDecoder; + frame.checkpoint_uncompressed_raw_bytes.resize(uc_enc_size, 0); + let mut decoder = ZlibDecoder::new(rply); + std::io::copy(&mut decoder, &mut std::io::Cursor::new(frame.checkpoint_uncompressed_raw_bytes.as_mut_slice()))?; + }, + Compression::Zstd => { + use zstd::Decoder; + frame.checkpoint_uncompressed_raw_bytes.resize(uc_enc_size, 0); + let mut decoder = Decoder::with_buffer(rply)?.single_frame(); + std::io::copy(&mut decoder, &mut std::io::Cursor::new(frame.checkpoint_uncompressed_raw_bytes.as_mut_slice()))?; + decoder.finish(); + }, + }; + // maybe decode + match encoding { + Encoding::Raw => {}, + Encoding::Statestream => { + frame.checkpoint_uncompressed_unencoded_bytes.resize(uc_ue_size, 0); + // statestream_decode(frame.checkpoint_decompressed_data().unwrap(), &mut frame.checkpoint_uncompressed_unencoded_bytes); + }, + } + } + } + Ok(()) +}