sbof 1.2.0

Small Binary Object Format
Documentation
use std::{
    collections::HashMap,
    fs,
    io::{self, ErrorKind, Write},
    panic::{catch_unwind, resume_unwind},
};

use rand::{random, random_range};
use sbof::{Error, Result, from_bytes, to_bytes};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug, PartialEq)]
enum TestEnum {
    Unit,
    Newtype(u8),
    Tuple(u8, u16),
    Struct { field1: i32, field2: char },
}

#[test]
fn main() -> Result<()> {
    all()
}

fn all() -> Result<()> {
    let mut loaded = false;
    let test_struct = if fs::exists("failed_case.json")? {
        println!("loading failed_case.json...");
        let str = fs::read_to_string("failed_case.json")?;
        let test_struct: HashMap<String, TestEnum> =
            serde_json::from_str(&str).expect("could not parse failed_case.json");
        println!("done\n");
        loaded = true;
        test_struct
    } else {
        println!("random");
        let len = 0..random::<u8>() as usize;
        let entries = len.clone().map(|_| {
            (
                {
                    (0..4)
                        .map(|_| random_range::<u8, _>(65..(65 + 26)) as char)
                        .collect::<String>()
                },
                match random::<u8>() % 4 {
                    0 => TestEnum::Unit,
                    1 => TestEnum::Newtype(random()),
                    2 => TestEnum::Tuple(random(), random()),
                    _ => TestEnum::Struct {
                        field1: random(),
                        field2: random(),
                    },
                },
            )
        });

        let mut map = HashMap::new();
        for (k, v) in entries {
            map.insert(k, v);
        }
        map
    };

    let bytes = to_bytes(&test_struct)?;
    println!("serialization complete");
    let unwind = catch_unwind(|| from_bytes::<HashMap<String, TestEnum>>(&bytes));
    let deser = match unwind {
        Err(e) => {
            failed_case(&test_struct, &bytes, loaded)?;
            resume_unwind(e)
        }
        Ok(Err(e)) => {
            failed_case(&test_struct, &bytes, loaded)?;
            io::stdout().flush()?;
            Err(e)?
        }
        Ok(Ok(deser)) => deser,
    };

    if test_struct != deser {
        failed_case_eq(&test_struct, &deser, &bytes)?;
        return Err(Error::Io(std::io::Error::new(
            ErrorKind::InvalidData,
            "failed",
        )));
    } else {
        println!("success!");
        let _ = fs::remove_file("failed_case.bin");
        let _ = fs::remove_file("failed_case.json");
        let _ = fs::remove_file("deser.json");
    }

    Ok(())
}

fn failed_case(test_struct: &HashMap<String, TestEnum>, bytes: &[u8], loaded: bool) -> Result<()> {
    if !loaded {
        fs::write(
            "failed_case.json",
            serde_json::to_string_pretty(test_struct).expect("failed to serialize to JSON"),
        )?;
        println!("wrote input to failed_case.json");
    }
    fs::write("failed_case.bin", bytes)?;
    println!("wrote serialized input to failed_case.bin");
    Ok(())
}

fn failed_case_eq(
    test_struct: &HashMap<String, TestEnum>,
    deser: &HashMap<String, TestEnum>,
    bytes: &[u8],
) -> Result<()> {
    println!("\n\nfound bad input: {test_struct:?}");
    fs::write(
        "failed_case.json",
        serde_json::to_string_pretty(test_struct).expect("failed to serialize to JSON"),
    )?;
    println!("wrote input to failed_case.json");
    fs::write("failed_case.bin", bytes)?;
    println!("wrote serialized input to failed_case.bin");
    fs::write(
        "deser.json",
        serde_json::to_string_pretty(deser).expect("failed to serialize to JSON"),
    )?;
    println!("wrote deserialized output to deser.json");
    println!("use `diff failed_case.json deser.json` to find differences");
    Ok(())
}