Files
HexPigeon/parser-wasm/tests/integration.rs
T
delikesanceandCursor 71974f5107 Add WASM parser and Vue dissector UI.
Ship a working HexPigeon: typed binary parsing in Rust/WASM, a three-pane frontend, and unit tests covering the field types.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-03 16:26:52 +00:00

298 lines
10 KiB
Rust

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:?}"),
}
}