use hex_pigeon_parser::checksum::{compute_crc16, compute_crc32}; use hex_pigeon_parser::{ parse_fields, CountSpec, Endian, FieldDef, FieldKind, FlagDef, ParsedValue, Schema, }; fn field(name: &str, kind: FieldKind) -> FieldDef { FieldDef { name: name.to_string(), kind, endian: None, description: None, flags: None, length: None, checksum_range: None, count: None, fields: None, } } fn field_len(name: &str, kind: FieldKind, length: usize) -> FieldDef { let mut f = field(name, kind); f.length = Some(length); f } #[test] fn schema_deserializes_from_json() { let json = r#"{ "name": "Test", "endian": "little", "fields": [{ "name": "id", "type": "uint8" }] }"#; let schema: Schema = serde_json::from_str(json).expect("schema JSON"); assert_eq!(schema.name.as_deref(), Some("Test")); assert_eq!(schema.endian, Endian::Little); assert_eq!(schema.fields.len(), 1); assert_eq!(schema.fields[0].kind, FieldKind::Uint8); } #[test] fn parses_integer_types_big_endian() { let data = [ 0x12, 0x12, 0x34, 0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC, 0xDE, 0xF0, 0xFE, 0xFF, 0xF0, 0x80, 0x00, 0x00, 0x01, ]; let fields = vec![ field("u8", FieldKind::Uint8), field("u16", FieldKind::Uint16), field("u32", FieldKind::Uint32), field("u64", FieldKind::Uint64), field("i8", FieldKind::Int8), field("i16", FieldKind::Int16), field("i32", FieldKind::Int32), ]; let result = parse_fields(&data, &fields).expect("parse"); assert_eq!(result.consumed_bytes, 22); assert!(matches!(result.fields[0].value, ParsedValue::Uint(0x12))); assert!(matches!(result.fields[1].value, ParsedValue::Uint(0x1234))); assert!(matches!(result.fields[2].value, ParsedValue::Uint(0x12345678))); assert!(matches!(result.fields[3].value, ParsedValue::Uint(0x123456789ABCDEF0))); assert!(matches!(result.fields[4].value, ParsedValue::Int(-2))); assert!(matches!(result.fields[5].value, ParsedValue::Int(-16))); assert!(matches!(result.fields[6].value, ParsedValue::Int(-2147483647))); } #[test] fn parses_integer_types_little_endian() { let data = [0x34, 0x12]; let mut f = field("u16", FieldKind::Uint16); f.endian = Some(Endian::Little); let result = parse_fields(&data, &[f]).expect("parse"); assert!(matches!(result.fields[0].value, ParsedValue::Uint(0x1234))); } #[test] fn parses_float_types() { let f32_bits = 1.0f32.to_bits(); let f64_bits = 2.5f64.to_bits(); let mut data = Vec::new(); data.extend_from_slice(&f32_bits.to_be_bytes()); data.extend_from_slice(&f64_bits.to_be_bytes()); let fields = vec![field("f32", FieldKind::Float32), field("f64", FieldKind::Float64)]; let result = parse_fields(&data, &fields).expect("parse"); match &result.fields[0].value { ParsedValue::Float(v) => assert!((v - 1.0).abs() < f64::EPSILON), other => panic!("expected float32, got {other:?}"), } match &result.fields[1].value { ParsedValue::Float(v) => assert!((v - 2.5).abs() < f64::EPSILON), other => panic!("expected float64, got {other:?}"), } } #[test] fn parses_bitflags() { let mut f8 = field("flags8", FieldKind::Bitflags8); f8.flags = Some(vec![ FlagDef { bit: 0, name: "A".into(), description: None, }, FlagDef { bit: 3, name: "D".into(), description: None, }, ]); let mut f16 = field("flags16", FieldKind::Bitflags16); f16.flags = Some(vec![FlagDef { bit: 1, name: "B".into(), description: None, }]); let result = parse_fields(&[0b0000_0101, 0x00, 0b0000_0010], &[f8, f16]).expect("parse"); match &result.fields[0].value { ParsedValue::Flags(flags) => { assert!(flags.iter().find(|f| f.name == "A").unwrap().set); assert!(!flags.iter().find(|f| f.name == "D").unwrap().set); } other => panic!("expected flags8, got {other:?}"), } match &result.fields[1].value { ParsedValue::Flags(flags) => assert!(flags[0].set), other => panic!("expected flags16, got {other:?}"), } } #[test] fn parses_bytes_padding_and_strings() { let data = [ 0xAA, 0xBB, 0xCC, // bytes[3] 0x00, 0x00, // padding[2] b'H', b'i', 0x00, 0x00, // string_fixed[4] -> "Hi" 0x03, b'F', b'O', b'O', // string_lp8 0x00, 0x02, b'B', b'R', // string_lp16 big-endian length ]; let fields = vec![ field_len("raw", FieldKind::Bytes, 3), field_len("pad", FieldKind::Padding, 2), field_len("fixed", FieldKind::StringFixed, 4), field("lp8", FieldKind::StringLp8), field("lp16", FieldKind::StringLp16), ]; let result = parse_fields(&data, &fields).expect("parse"); assert!(matches!(&result.fields[0].value, ParsedValue::Bytes(v) if v == &[0xAA, 0xBB, 0xCC])); assert!(matches!(result.fields[1].value, ParsedValue::None)); assert!(matches!(&result.fields[2].value, ParsedValue::Str(s) if s == "Hi")); assert!(matches!(&result.fields[3].value, ParsedValue::Str(s) if s == "FOO")); assert!(matches!(&result.fields[4].value, ParsedValue::Str(s) if s == "BR")); } #[test] fn validates_crc16_and_crc32() { let payload = [0x01, 0x02, 0x03, 0x04]; let crc16 = compute_crc16(&payload); let crc32 = compute_crc32(&payload); let mut data = payload.to_vec(); data.extend_from_slice(&crc16.to_be_bytes()); data.extend_from_slice(&crc32.to_be_bytes()); let mut crc16_field = field("c16", FieldKind::Crc16); crc16_field.checksum_range = Some([0, 4]); let mut crc32_field = field("c32", FieldKind::Crc32); crc32_field.checksum_range = Some([0, 4]); let fields = vec![ field_len("payload", FieldKind::Bytes, 4), crc16_field, crc32_field, ]; let result = parse_fields(&data, &fields).expect("parse"); assert_eq!(result.fields[1].checksum_valid, Some(true)); assert_eq!(result.fields[2].checksum_valid, Some(true)); let mut bad = data.clone(); bad[6] ^= 0xFF; let mut crc32_bad = field("c32", FieldKind::Crc32); crc32_bad.checksum_range = Some([0, 4]); let fields = vec![ field_len("payload", FieldKind::Bytes, 4), crc32_bad, ]; let result = parse_fields(&bad, &fields).expect("parse"); assert_eq!(result.fields[1].checksum_valid, Some(false)); } #[test] fn parses_repeated_fixed_and_dynamic_count() { let data = [0x02, 0x10, 0x20, 0x11, 0x21]; let repeated_fixed = FieldDef { name: "items".into(), kind: FieldKind::Repeated, endian: None, description: None, flags: None, length: None, checksum_range: None, count: Some(CountSpec::Fixed(2)), fields: Some(vec![field("val", FieldKind::Uint8)]), }; let result = parse_fields(&[0x10, 0x20, 0x11, 0x21], &[repeated_fixed]).expect("parse"); let children = result.fields[0].children.as_ref().expect("children"); assert_eq!(children.len(), 2); assert!(matches!(children[0].value, ParsedValue::Uint(0x10))); assert!(matches!(children[1].value, ParsedValue::Uint(0x20))); let repeated_dynamic = FieldDef { name: "list".into(), kind: FieldKind::Repeated, endian: None, description: None, flags: None, length: None, checksum_range: None, count: Some(CountSpec::Field("count".into())), fields: Some(vec![field("val", FieldKind::Uint8)]), }; let result = parse_fields(&data, &[field("count", FieldKind::Uint8), repeated_dynamic]).expect("parse"); let children = result.fields[1].children.as_ref().expect("children"); assert_eq!(children.len(), 2); } #[test] fn errors_on_truncated_buffer() { let result = parse_fields(&[0x01], &[field("u16", FieldKind::Uint16)]); assert!(result.is_err()); assert!(result.unwrap_err().contains("Not enough bytes")); } #[test] fn errors_on_missing_required_field_options() { let err = parse_fields(&[0x01], &[field("raw", FieldKind::Bytes)]).unwrap_err(); assert!(err.contains("requires 'length'")); let repeated = FieldDef { name: "r".into(), kind: FieldKind::Repeated, endian: None, description: None, flags: None, length: None, checksum_range: None, count: Some(CountSpec::Fixed(1)), fields: None, }; let err = parse_fields(&[0x01], &[repeated]).unwrap_err(); assert!(err.contains("requires 'fields'")); } #[test] fn parses_example_protocol_frame() { let header = [ 0xFF, 0x05, b'H', b'e', b'l', b'l', b'o', 0x00, 0x01, 0x02, 0x03, b'A', b'B', b'C', b'D', ]; let crc32 = compute_crc32(&header[..11]); let mut data = header.to_vec(); data.extend_from_slice(&crc32.to_be_bytes()); data.extend_from_slice(&[0x00, 0x03]); // flags: ACK + SYN let schema_json = include_str!("../../frontend/src/fixtures/example-schema.json"); let schema: Schema = serde_json::from_str(schema_json).expect("example schema"); let result = parse_fields(&data, &schema.fields).expect("example frame"); assert_eq!(result.total_bytes, data.len()); assert_eq!(result.consumed_bytes, data.len()); assert_eq!(result.fields.len(), 7); assert!(matches!(result.fields[0].value, ParsedValue::Uint(0xFF))); assert!(matches!(&result.fields[2].value, ParsedValue::Str(s) if s == "Hello")); assert!(matches!(&result.fields[4].value, ParsedValue::Str(s) if s == "ABCD")); assert_eq!(result.fields[5].checksum_valid, Some(true)); match &result.fields[6].value { ParsedValue::Flags(flags) => { assert!(flags.iter().find(|f| f.name == "ACK").unwrap().set); assert!(flags.iter().find(|f| f.name == "SYN").unwrap().set); assert!(!flags.iter().find(|f| f.name == "FIN").unwrap().set); } other => panic!("expected flags, got {other:?}"), } }