one step at a time
This commit is contained in:
@@ -4,5 +4,7 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
flate2 = { version = "1.1.5", features = ["zlib-rs"] }
|
||||||
rmp = "0.8.14"
|
rmp = "0.8.14"
|
||||||
thiserror = "2.0.17"
|
thiserror = "2.0.17"
|
||||||
|
zstd = "0.13.3"
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
+8
-8
@@ -1,8 +1,5 @@
|
|||||||
mod rply;
|
mod rply;
|
||||||
|
pub use rply::*;
|
||||||
pub fn add(left: u64, right: u64) -> u64 {
|
|
||||||
left + right
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
@@ -10,17 +7,20 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn v2_header() {
|
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() {
|
let header = match rply::read_header(&mut file).unwrap() {
|
||||||
rply::Header::V0V1(_) => panic!("Version too low"),
|
rply::Header::V0V1(_) => panic!("Version too low"),
|
||||||
rply::Header::V2(h) => h,
|
rply::Header::V2(h) => h,
|
||||||
};
|
};
|
||||||
// content_crc: 2199475946,
|
|
||||||
// initial_state_size: 2531,
|
|
||||||
// identifier: 1761326589,
|
|
||||||
assert_eq!(header.base.version, 2);
|
assert_eq!(header.base.version, 2);
|
||||||
assert_eq!(header.base.content_crc, 2199475946);
|
assert_eq!(header.base.content_crc, 2199475946);
|
||||||
assert_eq!(header.base.initial_state_size, 2531);
|
assert_eq!(header.base.initial_state_size, 2531);
|
||||||
assert_eq!(header.base.identifier, 1761326589);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+243
-15
@@ -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)]
|
#[repr(usize)]
|
||||||
pub enum HeaderV0V1Part {
|
pub enum HeaderV0V1Part {
|
||||||
Magic = 0,
|
Magic = 0,
|
||||||
@@ -9,11 +19,6 @@ pub enum HeaderV0V1Part {
|
|||||||
}
|
}
|
||||||
#[repr(usize)]
|
#[repr(usize)]
|
||||||
enum HeaderV2Part {
|
enum HeaderV2Part {
|
||||||
Magic = 0,
|
|
||||||
Version = 4,
|
|
||||||
CRC = 8,
|
|
||||||
StateSize = 12,
|
|
||||||
Identifier = 16,
|
|
||||||
FrameCount = 24,
|
FrameCount = 24,
|
||||||
BlockSize = 28,
|
BlockSize = 28,
|
||||||
SuperblockSize = 32,
|
SuperblockSize = 32,
|
||||||
@@ -35,6 +40,16 @@ pub enum FrameToken {
|
|||||||
Checkpoint = b'c',
|
Checkpoint = b'c',
|
||||||
Checkpoint2 = b'C',
|
Checkpoint2 = b'C',
|
||||||
}
|
}
|
||||||
|
impl From<u8> 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)]
|
#[repr(u8)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
@@ -45,27 +60,61 @@ pub enum SSToken {
|
|||||||
NewSuperblock = 2,
|
NewSuperblock = 2,
|
||||||
SuperblockSeq = 3,
|
SuperblockSeq = 3,
|
||||||
}
|
}
|
||||||
|
impl TryFrom<u8> for SSToken {
|
||||||
|
type Error = InvalidDeterminant;
|
||||||
|
|
||||||
|
fn try_from(value: u8) -> std::result::Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
0 => Ok(SSToken::Start),
|
||||||
|
1 => Ok(SSToken::NewBlock),
|
||||||
|
2 => Ok(SSToken::NewSuperblock),
|
||||||
|
3 => Ok(SSToken::SuperblockSeq),
|
||||||
|
_ => Err(InvalidDeterminant(value)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
#[derive(Debug)]
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
pub enum Compression {
|
pub enum Compression {
|
||||||
None = 0,
|
None = 0,
|
||||||
Zlib = 1,
|
Zlib = 1,
|
||||||
Zstd = 2,
|
Zstd = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<u8> for Compression {}
|
impl TryFrom<u8> for Compression {
|
||||||
|
type Error = InvalidDeterminant;
|
||||||
|
|
||||||
|
fn try_from(value: u8) -> std::result::Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
0 => Ok(Compression::None),
|
||||||
|
1 => Ok(Compression::Zlib),
|
||||||
|
2 => Ok(Compression::Zstd),
|
||||||
|
_ => Err(InvalidDeterminant(value)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
#[derive(Debug)]
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
pub enum Encoding {
|
pub enum Encoding {
|
||||||
Raw = 0,
|
Raw = 0,
|
||||||
Statestream = 1,
|
Statestream = 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<u8> for Encoding {}
|
impl TryFrom<u8> for Encoding {
|
||||||
|
type Error = InvalidDeterminant;
|
||||||
|
|
||||||
|
fn try_from(value: u8) -> std::result::Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
0 => Ok(Encoding::Raw),
|
||||||
|
1 => Ok(Encoding::Statestream),
|
||||||
|
_ => Err(InvalidDeterminant(value)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct HeaderBase {
|
pub struct HeaderBase {
|
||||||
@@ -92,8 +141,6 @@ pub enum Header {
|
|||||||
V2(HeaderV2),
|
V2(HeaderV2),
|
||||||
}
|
}
|
||||||
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum ReplayError {
|
pub enum ReplayError {
|
||||||
#[error("Invalid replay magic {0}")]
|
#[error("Invalid replay magic {0}")]
|
||||||
@@ -101,15 +148,19 @@ pub enum ReplayError {
|
|||||||
#[error("Unsupported version {0}")]
|
#[error("Unsupported version {0}")]
|
||||||
Version(u32),
|
Version(u32),
|
||||||
#[error("Unsupported compression scheme {0}")]
|
#[error("Unsupported compression scheme {0}")]
|
||||||
Compression(u8),
|
Compression(#[from] InvalidDeterminant),
|
||||||
#[error("I/O Error")]
|
#[error("I/O Error")]
|
||||||
IO(#[from] std::io::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<T> = std::result::Result<T, ReplayError>;
|
type Result<T> = std::result::Result<T, ReplayError>;
|
||||||
|
|
||||||
pub fn read_header(rply: &mut impl std::io::Read) -> Result<Header> {
|
pub fn read_header(rply: &mut impl std::io::BufRead) -> Result<Header> {
|
||||||
let mut bytes = vec![0; HEADER_LEN_BYTES];
|
let mut bytes = [0; HEADER_LEN_BYTES];
|
||||||
rply.read_exact(&mut bytes[0..HEADER_V0V1_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.
|
// 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.
|
// 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<Header> {
|
|||||||
if version < 2 {
|
if version < 2 {
|
||||||
return Ok(Header::V0V1(base));
|
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(
|
let frame_count = u32::from_le_bytes(
|
||||||
<[u8; 4]>::try_from(
|
<[u8; 4]>::try_from(
|
||||||
&bytes[(HeaderV2Part::FrameCount as usize)..(HeaderV2Part::FrameCount as usize + 4)],
|
&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<Header> {
|
|||||||
checkpoint_compression,
|
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<KeyData>,
|
||||||
|
pub input_events: Vec<InputData>,
|
||||||
|
checkpoint_raw_bytes: Vec<u8>,
|
||||||
|
checkpoint_uncompressed_raw_bytes: Vec<u8>,
|
||||||
|
checkpoint_uncompressed_unencoded_bytes: Vec<u8>,
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user