commit eec6d9f24cd436ea0f4bb1e878c3a653fdfa05c2 Author: Joseph C. Osborn Date: Fri Oct 24 13:53:37 2025 -0700 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4cc66ef --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "rply-codec" +version = "0.1.0" +edition = "2024" + +[dependencies] +rmp = "0.8.14" +thiserror = "2.0.17" diff --git a/examples/bobl.replay b/examples/bobl.replay new file mode 100644 index 0000000..b5a1cd3 Binary files /dev/null and b/examples/bobl.replay differ diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..78e1d0a --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,26 @@ +mod rply; + +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn v2_header() { + let mut file = 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); + } +} diff --git a/src/rply.rs b/src/rply.rs new file mode 100644 index 0000000..ab193f7 --- /dev/null +++ b/src/rply.rs @@ -0,0 +1,201 @@ +#[repr(usize)] +pub enum HeaderV0V1Part { + Magic = 0, + Version = 4, + CRC = 8, + StateSize = 12, + Identifier = 16, + HeaderLen = 24, +} +#[repr(usize)] +enum HeaderV2Part { + Magic = 0, + Version = 4, + CRC = 8, + StateSize = 12, + Identifier = 16, + FrameCount = 24, + BlockSize = 28, + SuperblockSize = 32, + CheckpointConfig = 36, + HeaderLen = 40, +} +const HEADER_V0V1_LEN_BYTES: usize = HeaderV0V1Part::HeaderLen as usize; +const HEADER_LEN_BYTES: usize = HeaderV2Part::HeaderLen as usize; + +const VERSION: u32 = 2; +const MAGIC: u32 = 0x42535632; + +#[repr(u8)] +#[non_exhaustive] +#[derive(Debug)] +pub enum FrameToken { + Invalid = 0, + Regular = b'f', + Checkpoint = b'c', + Checkpoint2 = b'C', +} + +#[repr(u8)] +#[non_exhaustive] +#[derive(Debug)] +pub enum SSToken { + Start = 0, + NewBlock = 1, + NewSuperblock = 2, + SuperblockSeq = 3, +} + +#[repr(u8)] +#[non_exhaustive] +#[derive(Debug)] +pub enum Compression { + None = 0, + Zlib = 1, + Zstd = 2, +} + +impl TryFrom for Compression {} + +#[repr(u8)] +#[non_exhaustive] +#[derive(Debug)] +pub enum Encoding { + Raw = 0, + Statestream = 1, +} + +impl TryFrom for Encoding {} + +#[derive(Debug)] +pub struct HeaderBase { + pub version: u32, + pub content_crc: u32, + pub initial_state_size: u32, + pub identifier: u64, +} + +#[derive(Debug)] +pub struct HeaderV2 { + pub base: HeaderBase, + pub frame_count: u32, + pub block_size: u32, + pub superblock_size: u32, + pub checkpoint_commit_interval: u8, + pub checkpoint_commit_threshold: u8, + pub checkpoint_compression: Compression, +} + +#[derive(Debug)] +pub enum Header { + V0V1(HeaderBase), + V2(HeaderV2), +} + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ReplayError { + #[error("Invalid replay magic {0}")] + Magic(u32), + #[error("Unsupported version {0}")] + Version(u32), + #[error("Unsupported compression scheme {0}")] + Compression(u8), + #[error("I/O Error")] + IO(#[from] std::io::Error), +} + +type Result = std::result::Result; + +pub fn read_header(rply: &mut impl std::io::Read) -> Result
{ + let mut bytes = vec![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. + let magic = u32::from_le_bytes( + <[u8; 4]>::try_from( + &bytes[(HeaderV0V1Part::Magic as usize)..(HeaderV0V1Part::Magic as usize + 4)], + ) + .unwrap(), + ); + if magic != MAGIC { + return Err(ReplayError::Magic(magic)); + } + let version = u32::from_le_bytes( + <[u8; 4]>::try_from( + &bytes[(HeaderV0V1Part::Version as usize)..(HeaderV0V1Part::Version as usize + 4)], + ) + .unwrap(), + ); + if version > 2 { + return Err(ReplayError::Version(version)); + } + let content_crc = u32::from_le_bytes( + <[u8; 4]>::try_from( + &bytes[(HeaderV0V1Part::CRC as usize)..(HeaderV0V1Part::CRC as usize + 4)], + ) + .unwrap(), + ); + let initial_state_size = u32::from_le_bytes( + <[u8; 4]>::try_from( + &bytes[(HeaderV0V1Part::StateSize as usize)..(HeaderV0V1Part::StateSize as usize + 4)], + ) + .unwrap(), + ); + let identifier = u64::from_le_bytes( + <[u8; 8]>::try_from( + &bytes + [(HeaderV0V1Part::Identifier as usize)..(HeaderV0V1Part::Identifier as usize + 8)], + ) + .unwrap(), + ); + let base = HeaderBase { + version, + content_crc, + initial_state_size, + identifier, + }; + if version < 2 { + return Ok(Header::V0V1(base)); + } + 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)], + ) + .unwrap(), + ); + let block_size = u32::from_le_bytes( + <[u8; 4]>::try_from( + &bytes[(HeaderV2Part::BlockSize as usize)..(HeaderV2Part::BlockSize as usize + 4)], + ) + .unwrap(), + ); + let superblock_size = u32::from_le_bytes( + <[u8; 4]>::try_from( + &bytes[(HeaderV2Part::SuperblockSize as usize) + ..(HeaderV2Part::SuperblockSize as usize + 4)], + ) + .unwrap(), + ); + let cp_config = u32::from_le_bytes( + <[u8; 4]>::try_from( + &bytes[(HeaderV2Part::CheckpointConfig as usize) + ..(HeaderV2Part::CheckpointConfig as usize + 4)], + ) + .unwrap(), + ); + let checkpoint_commit_interval = (cp_config >> 24) as u8; + let checkpoint_commit_threshold = ((cp_config >> 16) & 0xFF) as u8; + let checkpoint_compression = Compression::try_from(((cp_config >> 8) & 0xFF) as u8)?; + Ok(Header::V2(HeaderV2 { + base, + frame_count, + block_size, + superblock_size, + checkpoint_commit_interval, + checkpoint_commit_threshold, + checkpoint_compression, + })) +}