A Rust crate for simplifying the process of parsing binary file data into various Rust data types using the TypeCast macro.
TODO:
- Create Tests
- Better Error Handling
The macro is designed to simplify parsing data records in a binary file via usage of a text from a description file, in the example, a desc.xml
file. The following demonstrates how to define a custom enum called DataTypes
and use the TypeCast
macro.
In this example define DataTypes
, and use the cast attribute for each variant.
#[derive(Clone, Copy, Debug, Serialize, Deserialize, TypeCast)]
pub enum DataTypes {
#[cast(from_le_bytes => f32)]
AnyCustomVariant,
#[cast(from_le_bytes => [f32;2])]
AnyCustomVariant2
}
This will automatically generate a DataTypesCast enum:
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum DataTypesCast {
AnyCustomVariant(f32),
AnyCustomVariant2([f32;2]),
}
Any Enum that utilizes the TypeCast will simply have Cast
appended to the name. This:
#[derive(TypeCast)]
enum ExampleEnum {
//
}
will generate the following at compile time:
#[derive(Clone, Debug, Serialize, Deserialize)]
enum ExampleEnumCast {
//
}
The attribute macro #[derive(Clone, Debug, Serialize, Deserialize)]
is hardcoded above the enum {}Cast
at the moment. If there is significant demand, the macro can be altered to include only those defined on the parent enum.
Okay, but why not just define the DataTypesCast or ExampleEnumCast and skip the attribute nonsense?
Because the following are automatically generated:
impl DataTypes {
pub fn parse(self, input: &[u8]) -> IResult<&[u8], DataTypesCast> {
match self {
DataTypes::AnyCustomVariant => {
let (tail, bytes) = nom::number::complete::le_f32(input)?;
Ok((tail, DataTypesCast::AnyCustomVariant(bytes)))
}
// This can be generated for any sized array supported by the standard library
DataTypes::AnyCustomVariant2 => {
let (tail, elements_vec) = nom::multi::count(
nom::number::complete::le_f32,
2,
)(input)?;
let out: [f32; 2] = elements_vec
.try_into()
.map_err(|_| nom::Err::Failure(
nom::error::Error::new(input, nom::error::ErrorKind::Fail),
))?;
Ok((tail, DataTypesCast::AnyCustomVariant2(out)))
}
}
}
// The TryInto implementations are so the values can be used outside of match statements. Right now, its tedious to use, but it works. See the hashmapped_fields example for usage.
impl std::convert::TryInto<f32> for DataTypesCast {
type Error = String;
fn try_into(self) -> Result<f32, Self::Error> {
match self {
DataTypesCast::AnyCustomVariant(val) => Ok(val),
_ => {
Err({
let res = ::alloc::fmt::format(
format_args!(
"Cannot convert non-compatible DataTypesCast into {0}",
"f32"
),
);
res
})
}
}
}
}
// allows
impl std::convert::TryInto<[f32; 2]> for DataTypesCast {
type Error = String;
fn try_into(self) -> Result<[f32; 2], Self::Error> {
match self {
DataTypesCast::AnyCustomVariant2(val) => Ok(val),
_ => {
Err({
let res = ::alloc::fmt::format(
format_args!(
"Cannot convert non-compatible DataTypesCast into {0}",
"[f32 ; 2]"
),
);
res
})
}
}
}
}
}
// Generated at compile time for FromStr trait:
impl std::str::FromStr for DataTypes {
type Err = Box<dyn std::error::Error + Send + Sync>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"AnyCustomVariant" => Ok(DataTypes::AnyCustomVariant),
"AnyCustomVariant2" => Ok(DataTypes::AnyCustomVariant2),
_ => Err("Invalid variant".into()),
}
}
}
If the parent enum has a large number of variants, this would be extremely tedious to type out.
Define your data structures and functions for parsing the binary data.
In the hashmapped_fields example, DataRecord
is a struct that contains a HashMap of field names and their parsed values. RecordDescs
is a struct that deserializes description information from the desc.xml
whose descriptions are then used to parse the data.dat
file into a DataRecord
. The example prints out the parsed data in the DataTypesCast
variants and then extracts and prints the values output from a match statement and output individually using try_into
.