use binrw::{BinRead, BinResult, Endian};
use binrw::io::{Read, Seek, SeekFrom};
use ssbh_write::SsbhWrite;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::{absolute_offset_checked, RelPtr64};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[derive(Debug)]
pub struct SsbhEnum64<T: DataType> {
pub data: RelPtr64<T>,
}
pub trait DataType {
fn data_type(&self) -> u64;
}
impl<T: DataType + PartialEq> PartialEq for SsbhEnum64<T> {
fn eq(&self, other: &Self) -> bool {
self.data == other.data
}
}
impl<T: DataType + Clone> Clone for SsbhEnum64<T> {
fn clone(&self) -> Self {
Self {
data: self.data.clone(),
}
}
}
impl<T> BinRead for SsbhEnum64<T>
where
T: DataType + for<'a> BinRead<Args<'a> = (u64,)> + crate::SsbhWrite,
{
type Args<'a> = ();
fn read_options<R: Read + Seek>(
reader: &mut R,
endian: Endian,
_args: Self::Args<'_>,
) -> BinResult<Self> {
let pos_before_read = reader.stream_position()?;
let relative_offset = u64::read_options(reader, endian, ())?;
let data_type = u64::read_options(reader, endian, ())?;
if relative_offset == 0 {
return Ok(SsbhEnum64 {
data: RelPtr64(None),
});
}
let saved_pos = reader.stream_position()?;
let seek_pos = absolute_offset_checked(pos_before_read, relative_offset)?;
reader.seek(SeekFrom::Start(seek_pos))?;
let value = T::read_options(reader, endian, (data_type,))?;
reader.seek(SeekFrom::Start(saved_pos))?;
Ok(SsbhEnum64 {
data: RelPtr64::new(value),
})
}
}
impl<T: DataType + SsbhWrite> SsbhWrite for SsbhEnum64<T> {
fn ssbh_write<W: std::io::Write + std::io::Seek>(
&self,
writer: &mut W,
data_ptr: &mut u64,
) -> std::io::Result<()> {
let current_pos = writer.stream_position()?;
if *data_ptr < current_pos + self.size_in_bytes() {
*data_ptr = current_pos + self.size_in_bytes();
}
self.data.ssbh_write(writer, data_ptr)?;
self.data
.as_ref()
.map(DataType::data_type)
.unwrap_or(0)
.ssbh_write(writer, data_ptr)?;
Ok(())
}
fn size_in_bytes(&self) -> u64 {
8 + 8
}
}
macro_rules! ssbh_enum {
($(#[$attr1:meta])* $name:ident, $($(#[$attr2:meta])* $tag:literal => $variant:ident($body:tt)),*) => {
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[derive(Debug, BinRead, SsbhWrite, PartialEq, Clone)]
#[br(import(data_type: u64))]
$(#[$attr1])*
pub enum $name {
$(
$(#[$attr2])*
#[br(pre_assert(data_type == $tag))]
$variant($body)
),*
}
impl crate::DataType for $name {
fn data_type(&self) -> u64 {
match self {
$(
Self::$variant(_) => $tag
),*
}
}
}
};
}
pub(crate) use ssbh_enum;
#[cfg(test)]
mod tests {
use super::*;
use binrw::io::Cursor;
use binrw::BinReaderExt;
use hexlit::hex;
ssbh_enum!(
TestData,
1 => Float(f32),
2 => Unsigned(u32)
);
#[test]
fn read_ssbh_enum_float() {
let mut reader = Cursor::new(hex!("10000000 00000000 01000000 00000000 0000803F"));
let value = reader.read_le::<SsbhEnum64<TestData>>().unwrap();
assert_eq!(TestData::Float(1.0f32), value.data.0.unwrap());
let value = reader.read_le::<f32>().unwrap();
assert_eq!(1.0f32, value);
}
#[test]
fn read_ssbh_enum_unsigned() {
let mut reader = Cursor::new(hex!("10000000 00000000 02000000 00000000 04000000"));
let value = reader.read_le::<SsbhEnum64<TestData>>().unwrap();
assert_eq!(TestData::Unsigned(4u32), value.data.0.unwrap());
}
#[test]
fn read_ssbh_enum_offset_overflow() {
let mut reader = Cursor::new(hex!(
"00000000 FFFFFFFF FFFFFFFF 02000000 00000000 04000000"
));
reader.seek(SeekFrom::Start(4)).unwrap();
let result = reader.read_le::<SsbhEnum64<TestData>>();
assert!(matches!(
result,
Err(binrw::error::Error::AssertFail { pos: 4, message })
if message == format!(
"Overflow occurred while computing relative offset {}",
0xFFFFFFFFFFFFFFFFu64
)
));
let value = reader.read_le::<u32>().unwrap();
assert_eq!(4u32, value);
}
#[test]
fn ssbh_write_enum_float() {
let value = SsbhEnum64::<TestData> {
data: RelPtr64::new(TestData::Float(1.0f32)),
};
let mut writer = Cursor::new(Vec::new());
let mut data_ptr = 0;
value.ssbh_write(&mut writer, &mut data_ptr).unwrap();
assert_eq!(
writer.into_inner(),
hex!("10000000 00000000 01000000 00000000 0000803F")
);
}
#[test]
fn ssbh_write_enum_unsigned() {
let value = SsbhEnum64::<TestData> {
data: RelPtr64::new(TestData::Unsigned(5u32)),
};
let mut writer = Cursor::new(Vec::new());
let mut data_ptr = 0;
value.ssbh_write(&mut writer, &mut data_ptr).unwrap();
assert_eq!(
writer.into_inner(),
hex!("10000000 00000000 02000000 00000000 05000000")
);
}
}