pub mod formats;
mod arrays;
pub use arrays::{SsbhArray, SsbhByteBuffer};
mod vectors;
pub use vectors::{Color4f, Matrix3x3, Matrix4x4, Vector3, Vector4};
mod strings;
pub use strings::{CString, CString1, SsbhString, SsbhString8};
mod enums;
pub use enums::{DataType, SsbhEnum64};
pub(crate) use enums::ssbh_enum;
pub mod prelude {
pub use crate::formats::adj::Adj;
pub use crate::formats::anim::Anim;
pub use crate::formats::hlpb::Hlpb;
pub use crate::formats::matl::Matl;
pub use crate::formats::mesh::Mesh;
pub use crate::formats::meshex::MeshEx;
pub use crate::formats::modl::Modl;
pub use crate::formats::nlst::Nlst;
pub use crate::formats::nrpd::Nrpd;
pub use crate::formats::nufx::Nufx;
pub use crate::formats::shdr::Shdr;
pub use crate::formats::skel::Skel;
pub use crate::{Ssbh, SsbhFile};
}
use self::formats::*;
use binrw::io::Cursor;
use binrw::{binread, BinReaderExt};
use binrw::{
io::{Read, Seek, SeekFrom},
BinRead, BinResult, Endian,
};
use thiserror::Error;
use binrw::io::Write;
use ssbh_write::SsbhWrite;
use std::convert::TryFrom;
use std::fs;
use std::marker::PhantomData;
use std::path::Path;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
impl SsbhFile {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, ReadSsbhError> {
let mut file = Cursor::new(fs::read(path)?);
let ssbh = file.read_le::<SsbhFile>()?;
Ok(ssbh)
}
pub fn read<R: Read + Seek>(reader: &mut R) -> Result<Self, ReadSsbhError> {
let ssbh = reader.read_le::<SsbhFile>()?;
Ok(ssbh)
}
pub fn write<W: std::io::Write + Seek>(&self, writer: &mut W) -> std::io::Result<()> {
write_ssbh_header_and_data(writer, &self.data)?;
Ok(())
}
pub fn write_to_file<P: AsRef<Path>>(&self, path: P) -> std::io::Result<()> {
let mut file = std::fs::File::create(path)?;
write_buffered(&mut file, |c| write_ssbh_header_and_data(c, &self.data))?;
Ok(())
}
}
#[derive(Debug, Error)]
pub enum ReadSsbhError {
#[error(transparent)]
BinRead(#[from] binrw::error::Error),
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("the type of SSBH file did not match the expected SSBH type")]
InvalidSsbhType,
}
macro_rules! ssbh_read_write_impl {
($ty:path, $ty2:path, $magic:expr) => {
impl $ty {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, ReadSsbhError> {
let mut file = Cursor::new(fs::read(path)?);
let ssbh = file.read_le::<SsbhFile>()?;
match ssbh.data {
$ty2(v) => Ok(v.data),
_ => Err(ReadSsbhError::InvalidSsbhType),
}
}
pub fn read<R: Read + Seek>(reader: &mut R) -> Result<Self, ReadSsbhError> {
let ssbh = reader.read_le::<SsbhFile>()?;
match ssbh.data {
$ty2(v) => Ok(v.data),
_ => Err(ReadSsbhError::InvalidSsbhType),
}
}
pub fn write<W: std::io::Write + Seek>(&self, writer: &mut W) -> std::io::Result<()> {
write_ssbh_file(writer, self, $magic)?;
Ok(())
}
pub fn write_to_file<P: AsRef<Path>>(&self, path: P) -> std::io::Result<()> {
let mut file = std::fs::File::create(path)?;
write_buffered(&mut file, |c| write_ssbh_file(c, self, $magic))?;
Ok(())
}
}
};
}
macro_rules! read_write_impl {
($ty:path) => {
impl $ty {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Box<dyn std::error::Error>> {
let mut file = Cursor::new(fs::read(path)?);
let value = file.read_le::<$ty>()?;
Ok(value)
}
pub fn read<R: Read + Seek>(
reader: &mut R,
) -> Result<Self, Box<dyn std::error::Error>> {
let value = reader.read_le::<$ty>()?;
Ok(value)
}
pub fn write<W: std::io::Write + Seek>(&self, writer: &mut W) -> std::io::Result<()> {
SsbhWrite::write(self, writer)?;
Ok(())
}
pub fn write_to_file<P: AsRef<Path>>(&self, path: P) -> std::io::Result<()> {
let mut file = std::fs::File::create(path)?;
write_buffered(&mut file, |c| self.write(c))?;
Ok(())
}
}
};
}
ssbh_read_write_impl!(prelude::Hlpb, Ssbh::Hlpb, b"BPLH");
ssbh_read_write_impl!(prelude::Matl, Ssbh::Matl, b"LTAM");
ssbh_read_write_impl!(prelude::Modl, Ssbh::Modl, b"LDOM");
ssbh_read_write_impl!(prelude::Mesh, Ssbh::Mesh, b"HSEM");
ssbh_read_write_impl!(prelude::Skel, Ssbh::Skel, b"LEKS");
ssbh_read_write_impl!(prelude::Anim, Ssbh::Anim, b"MINA");
ssbh_read_write_impl!(prelude::Nlst, Ssbh::Nlst, b"TSLN");
ssbh_read_write_impl!(prelude::Nrpd, Ssbh::Nrpd, b"DPRN");
ssbh_read_write_impl!(prelude::Nufx, Ssbh::Nufx, b"XFUN");
ssbh_read_write_impl!(prelude::Shdr, Ssbh::Shdr, b"RDHS");
read_write_impl!(prelude::MeshEx);
read_write_impl!(prelude::Adj);
pub(crate) fn absolute_offset_checked(
position: u64,
relative_offset: u64,
) -> Result<u64, binrw::Error> {
match position.checked_add(relative_offset) {
Some(offset) => Ok(offset),
None => Err(binrw::error::Error::AssertFail {
pos: position,
message: format!("Overflow occurred while computing relative offset {relative_offset}"),
}),
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[derive(Debug, PartialEq, Clone)]
#[repr(transparent)]
pub struct Ptr<P, T>(
Option<T>,
#[cfg_attr(feature = "serde", serde(skip))] PhantomData<P>,
);
impl<P, T> Ptr<P, T> {
pub fn new(value: T) -> Self {
Self(Some(value), PhantomData::<P>)
}
pub fn null() -> Self {
Self(None, PhantomData::<P>)
}
}
pub type Ptr16<T> = Ptr<u16, T>;
pub type Ptr32<T> = Ptr<u32, T>;
pub type Ptr64<T> = Ptr<u64, T>;
impl<P, T> BinRead for Ptr<P, T>
where
P: BinRead + Default + PartialEq + Into<u64>,
T: BinRead,
for<'a> P: BinRead<Args<'a> = ()>,
for<'a> T::Args<'a>: Clone,
{
type Args<'a> = T::Args<'a>;
fn read_options<R: Read + Seek>(
reader: &mut R,
endian: Endian,
args: Self::Args<'_>,
) -> BinResult<Self> {
let offset = P::read_options(reader, endian, P::Args::default())?;
if offset == P::default() {
return Ok(Self::null());
}
let saved_pos = reader.stream_position()?;
reader.seek(SeekFrom::Start(offset.into()))?;
let value = T::read_options(reader, endian, args)?;
reader.seek(SeekFrom::Start(saved_pos))?;
Ok(Self::new(value))
}
}
impl<P, T> core::ops::Deref for Ptr<P, T> {
type Target = Option<T>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<P, T> core::ops::DerefMut for Ptr<P, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[derive(Debug)]
#[repr(transparent)]
pub struct RelPtr64<T>(Option<T>);
impl<T> RelPtr64<T> {
pub fn new(value: T) -> Self {
Self(Some(value))
}
pub fn null() -> Self {
Self(None)
}
}
impl<T: Clone> Clone for RelPtr64<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T: PartialEq> PartialEq for RelPtr64<T> {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl<T: Eq> Eq for RelPtr64<T> {}
impl<T> From<Option<T>> for RelPtr64<T> {
fn from(v: Option<T>) -> Self {
match v {
Some(v) => Self::new(v),
None => Self::null(),
}
}
}
impl<T> BinRead for RelPtr64<T>
where
T: BinRead,
for<'a> T::Args<'a>: Clone,
{
type Args<'a> = T::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, ())?;
if relative_offset == 0 {
return Ok(Self::null());
}
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, args)?;
reader.seek(SeekFrom::Start(saved_pos))?;
Ok(Self(Some(value)))
}
}
impl<T> core::ops::Deref for RelPtr64<T> {
type Target = Option<T>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> core::ops::DerefMut for RelPtr64<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[derive(BinRead, Debug)]
#[br(magic = b"HBSS")]
pub struct SsbhFile {
#[br(align_before = 0x10)]
pub data: Ssbh,
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[derive(BinRead, Debug)]
pub enum Ssbh {
#[br(magic = b"BPLH")]
Hlpb(Versioned<hlpb::Hlpb>),
#[br(magic = b"LTAM")]
Matl(Versioned<matl::Matl>),
#[br(magic = b"LDOM")]
Modl(Versioned<modl::Modl>),
#[br(magic = b"HSEM")]
Mesh(Versioned<mesh::Mesh>),
#[br(magic = b"LEKS")]
Skel(Versioned<skel::Skel>),
#[br(magic = b"MINA")]
Anim(Versioned<anim::Anim>),
#[br(magic = b"TSLN")]
Nlst(Versioned<nlst::Nlst>),
#[br(magic = b"DPRN")]
Nrpd(Versioned<nrpd::Nrpd>),
#[br(magic = b"XFUN")]
Nufx(Versioned<nufx::Nufx>),
#[br(magic = b"RDHS")]
Shdr(Versioned<shdr::Shdr>),
}
#[binread]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[derive(Debug)]
pub struct Versioned<T: BinRead<Args<'static> = (u16, u16)>> {
#[br(temp)]
major_version: u16,
#[br(temp)]
minor_version: u16,
#[br(args(major_version, minor_version))]
pub data: T,
}
impl<T> SsbhWrite for Versioned<T>
where
T: BinRead<Args<'static> = (u16, u16)> + SsbhWrite + Version,
{
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();
}
let (major_version, minor_version) = self.data.major_minor_version();
major_version.ssbh_write(writer, data_ptr)?;
minor_version.ssbh_write(writer, data_ptr)?;
self.data.ssbh_write(writer, data_ptr)?;
Ok(())
}
fn size_in_bytes(&self) -> u64 {
2 + 2 + self.data.size_in_bytes()
}
}
pub trait Version {
fn major_minor_version(&self) -> (u16, u16);
}
pub(crate) fn round_up(value: u64, n: u64) -> u64 {
((value + n - 1) / n) * n
}
pub(crate) fn write_relative_offset<W: Write + Seek>(
writer: &mut W,
data_ptr: &u64,
) -> std::io::Result<()> {
let current_pos = writer.stream_position()?;
u64::write(&(*data_ptr - current_pos), writer)?;
Ok(())
}
fn write_rel_ptr_aligned_specialized<
W: Write + Seek,
T,
F: Fn(&T, &mut W, &mut u64) -> std::io::Result<()>,
>(
writer: &mut W,
data: &Option<T>,
data_ptr: &mut u64,
alignment: u64,
write_t: F,
) -> std::io::Result<()> {
match data {
Some(value) => {
*data_ptr = round_up(*data_ptr, alignment);
write_relative_offset(writer, data_ptr)?;
let pos_after_offset = writer.stream_position()?;
writer.seek(SeekFrom::Start(*data_ptr))?;
write_t(value, writer, data_ptr)?;
let current_pos = writer.stream_position()?;
if current_pos > *data_ptr {
*data_ptr = round_up(current_pos, alignment);
}
writer.seek(SeekFrom::Start(pos_after_offset))?;
Ok(())
}
None => {
u64::write(&0u64, writer)?;
Ok(())
}
}
}
fn write_rel_ptr_aligned<W: Write + Seek, T: SsbhWrite>(
writer: &mut W,
data: &Option<T>,
data_ptr: &mut u64,
alignment: u64,
) -> std::io::Result<()> {
write_rel_ptr_aligned_specialized(writer, data, data_ptr, alignment, T::ssbh_write)?;
Ok(())
}
fn write_ssbh_header<W: Write + Seek>(writer: &mut W, magic: &[u8; 4]) -> std::io::Result<()> {
writer.write_all(b"HBSS")?;
u64::write(&64u64, writer)?;
u32::write(&0u32, writer)?;
writer.write_all(magic)?;
Ok(())
}
impl<P, T> SsbhWrite for Ptr<P, T>
where
P: SsbhWrite + Default + TryFrom<u64>,
T: SsbhWrite,
{
fn ssbh_write<W: Write + 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();
}
match &self.0 {
Some(value) => {
let alignment = T::alignment_in_bytes();
let current_pos = writer.stream_position()?;
if *data_ptr < current_pos + self.size_in_bytes() {
*data_ptr = current_pos + self.size_in_bytes();
}
*data_ptr = round_up(*data_ptr, alignment);
let offset = P::try_from(*data_ptr).map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::Other,
format!(
"Failed to convert offset {} to a pointer with {} bytes.",
data_ptr,
std::mem::size_of::<P>()
),
)
})?;
P::ssbh_write(&offset, writer, data_ptr)?;
let pos_after_offset = writer.stream_position()?;
writer.seek(SeekFrom::Start(*data_ptr))?;
value.ssbh_write(writer, data_ptr)?;
let current_pos = writer.stream_position()?;
if current_pos > *data_ptr {
*data_ptr = round_up(current_pos, alignment);
}
writer.seek(SeekFrom::Start(pos_after_offset))?;
Ok(())
}
None => {
P::default().ssbh_write(writer, data_ptr)?;
Ok(())
}
}
}
fn size_in_bytes(&self) -> u64 {
std::mem::size_of::<P>() as u64
}
}
impl<T: SsbhWrite> SsbhWrite for RelPtr64<T> {
fn ssbh_write<W: Write + 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();
}
write_rel_ptr_aligned(writer, &self.0, data_ptr, T::alignment_in_bytes())?;
Ok(())
}
fn size_in_bytes(&self) -> u64 {
8
}
}
pub(crate) fn write_ssbh_header_and_data<W: Write + Seek>(
writer: &mut W,
data: &Ssbh,
) -> std::io::Result<()> {
match &data {
Ssbh::Modl(modl) => write_ssbh_file(writer, &modl.data, b"LDOM"),
Ssbh::Skel(skel) => write_ssbh_file(writer, &skel.data, b"LEKS"),
Ssbh::Nufx(nufx) => write_ssbh_file(writer, &nufx.data, b"XFUN"),
Ssbh::Shdr(shdr) => write_ssbh_file(writer, &shdr.data, b"RDHS"),
Ssbh::Matl(matl) => write_ssbh_file(writer, &matl.data, b"LTAM"),
Ssbh::Anim(anim) => write_ssbh_file(writer, &anim.data, b"MINA"),
Ssbh::Hlpb(hlpb) => write_ssbh_file(writer, &hlpb.data, b"BPLH"),
Ssbh::Mesh(mesh) => write_ssbh_file(writer, &mesh.data, b"HSEM"),
Ssbh::Nrpd(nrpd) => write_ssbh_file(writer, &nrpd.data, b"DPRN"),
Ssbh::Nlst(nlst) => write_ssbh_file(writer, &nlst.data, b"TSLN"),
}
}
pub(crate) fn write_buffered<
W: Write + Seek,
F: Fn(&mut Cursor<Vec<u8>>) -> std::io::Result<()>,
>(
writer: &mut W,
write_data: F,
) -> std::io::Result<()> {
let mut cursor = Cursor::new(Vec::new());
write_data(&mut cursor)?;
writer.write_all(cursor.get_mut())?;
Ok(())
}
pub(crate) fn write_ssbh_file<W: Write + Seek, S: SsbhWrite + Version>(
writer: &mut W,
data: &S,
magic: &[u8; 4],
) -> std::io::Result<()> {
write_ssbh_header(writer, magic)?;
let mut data_ptr = writer.stream_position()?;
data_ptr += data.size_in_bytes() + 4; let (major_version, minor_version) = data.major_minor_version();
major_version.ssbh_write(writer, &mut data_ptr)?;
minor_version.ssbh_write(writer, &mut data_ptr)?;
data.ssbh_write(writer, &mut data_ptr)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use hexlit::hex;
#[test]
fn new_relptr64() {
let ptr = RelPtr64::new(5u32);
assert_eq!(Some(5u32), ptr.0);
}
#[test]
fn relptr64_from_option() {
assert_eq!(RelPtr64::new(5u32), Some(5u32).into());
assert_eq!(RelPtr64::<u32>::null(), None.into());
}
#[test]
fn read_relptr() {
let mut reader = Cursor::new(hex!("09000000 00000000 05070000"));
let value = reader.read_le::<RelPtr64<u8>>().unwrap();
assert_eq!(7u8, value.unwrap());
let value = reader.read_le::<u8>().unwrap();
assert_eq!(5u8, value);
}
#[test]
fn read_relptr_null() {
let mut reader = Cursor::new(hex!("00000000 00000000 05070000"));
let value = reader.read_le::<RelPtr64<u8>>().unwrap();
assert_eq!(None, value.0);
let value = reader.read_le::<u8>().unwrap();
assert_eq!(5u8, value);
}
#[test]
fn read_relptr_offset_overflow() {
let mut reader = Cursor::new(hex!("00000000 FFFFFFFF FFFFFFFF 05070000"));
reader.seek(SeekFrom::Start(4)).unwrap();
let result = reader.read_le::<RelPtr64<u8>>();
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::<u8>().unwrap();
assert_eq!(5u8, value);
}
#[test]
fn read_ptr8() {
let mut reader = Cursor::new(hex!("04050000 07"));
let value = reader.read_le::<Ptr<u8, u8>>().unwrap();
assert_eq!(7u8, value.unwrap());
let value = reader.read_le::<u8>().unwrap();
assert_eq!(5u8, value);
}
#[test]
fn read_ptr64() {
let mut reader = Cursor::new(hex!("09000000 00000000 05070000"));
let value = reader.read_le::<Ptr64<u8>>().unwrap();
assert_eq!(7u8, value.unwrap());
let value = reader.read_le::<u8>().unwrap();
assert_eq!(5u8, value);
}
#[test]
fn read_ptr_null() {
let mut reader = Cursor::new(hex!("00000000 00000000 05070000"));
let value = reader.read_le::<Ptr64<u8>>().unwrap();
assert_eq!(None, value.0);
let value = reader.read_le::<u8>().unwrap();
assert_eq!(5u8, value);
}
#[test]
fn write_ptr16() {
let value = Ptr16::<u8>::new(5u8);
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!("0200 05"));
assert_eq!(3, data_ptr);
}
#[test]
fn write_ptr32() {
let value = Ptr32::<u8>::new(5u8);
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!("04000000 05"));
assert_eq!(5, data_ptr);
}
#[test]
fn write_null_ptr32() {
let value = Ptr32::<u8>::null();
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!("00000000"));
assert_eq!(4, data_ptr);
}
#[test]
fn write_ptr64() {
let value = Ptr64::<u8>::new(5u8);
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!("08000000 00000000 05"));
assert_eq!(9, data_ptr);
}
#[test]
fn write_ptr64_vec_u8() {
let value = Ptr64::new(vec![5u8]);
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!("08000000 00000000 05"));
assert_eq!(9, data_ptr);
}
#[test]
fn write_ptr64_vec_u32() {
let value = Ptr64::new(vec![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!("08000000 00000000 05000000"));
assert_eq!(12, data_ptr);
}
#[test]
fn write_null_rel_ptr() {
let value = RelPtr64::<u32>(None);
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!("00000000 00000000"));
assert_eq!(8, data_ptr);
}
#[test]
fn write_nested_rel_ptr_depth2() {
let value = RelPtr64::new(RelPtr64::new(7u32));
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!(
"08000000 00000000
08000000 00000000
07000000"
)
);
assert_eq!(20, data_ptr);
}
}