//! Cross-implementation parity: Rust codec vs the .NET reference. //! //! Closes out the M1 DoD line in `design/60-roadmap.md`: //! //! > every Frida-captured write/advise/subscribe body that the .NET reference //! > encodes today round-trips byte-identical through `mxaccess-codec` — i.e. //! > the proven matrix in `work_remain.md` (scalar/array writes, advise/unadvise, //! > single-record `0x33` DataUpdate, single-record SubscriptionStatus, the //! > 5-byte `00 00 50 80 00` write-complete frame, and the 1-byte completion //! > frames `0x00`/`0x41`/`0xEF` preserved verbatim). Cross-validated against //! > `src/MxNativeCodec.Tests/` outputs. //! //! Each fixture in this file is **the canonical hex byte sequence the .NET //! `MxNativeCodec.Tests` program asserts against** today. They are copied //! verbatim from `src/MxNativeCodec.Tests/Program.cs` lines 4-100. The //! reference is the bytes themselves, not the source program — if the .NET //! reference's encoder ever changes, regenerate via //! `dotnet run --project src\MxNativeCodec.Tests\MxNativeCodec.Tests.csproj` //! and capture the new hex. //! //! The parity strategy mirrors the .NET `RunRoundTrip` helper //! (`Program.cs:119-156`): //! 1. Take the observed bytes. //! 2. Hand them to [`ObservedWriteBodyTemplate::from_observed`] to capture the //! prefix/suffix slots verbatim. //! 3. Call [`ObservedWriteBodyTemplate::with_value`] with the original value. //! 4. Assert byte-identical. //! //! `with_value` preserves the captured prefix (handle, clientToken stash) and //! suffix (including FILETIME for timestamped Write2 bodies), so a single //! parity check works uniformly for normal and timestamped fixtures. #![allow( clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing, clippy::panic )] use mxaccess_codec::{ MxValue, MxValueKind, NmxTransferEnvelopeTemplate, ObservedWriteBodyTemplate, }; /// Decode a space-separated hex string into bytes. Mirrors `Convert.FromHexString` /// in the .NET test helper. fn hex_to_bytes(s: &str) -> Vec { s.split_whitespace() .map(|tok| u8::from_str_radix(tok, 16).expect("malformed hex token in fixture")) .collect() } /// One write-body parity fixture. Mirrors a single `RunRoundTrip(...)` call /// in `src/MxNativeCodec.Tests/Program.cs`. struct Fixture { name: &'static str, kind: MxValueKind, /// Pre-built `MxValue` matching what the .NET test passes. For DateTime /// fixtures, the .NET helper formats the `DateTime` to `M/d/yyyy h:mm:ss tt` /// before encoding; we pass the same pre-formatted string via /// `MxValue::String` (the encoder collapses DateTime onto the String wire /// kind 0x05). value: MxValue, /// Canonical hex from the .NET test program. Space-separated bytes. hex: &'static str, } fn write_body_fixtures() -> Vec { vec![ // -------- Scalar Write (0x37) -------- Fixture { name: "int", kind: MxValueKind::Int32, value: MxValue::Int32(109), hex: "37 01 00 05 00 36 d7 02 00 9b 00 0a 00 3e da 00 00 02 6d 00 00 00 ff ff 00 00 00 00 00 00 00 00 c9 14 b1 08 01 00 00 00", }, Fixture { name: "bool", kind: MxValueKind::Boolean, value: MxValue::Boolean(true), hex: "37 01 00 05 00 36 d7 02 00 9a 00 0a 00 fa 7d 00 00 01 ff ff ff 00 00 00 00 00 00 00 00 e1 d8 b5 08 01 00 00 00", }, Fixture { name: "float", kind: MxValueKind::Float32, value: MxValue::Float32(1.25), hex: "37 01 00 05 00 36 d7 02 00 9c 00 0a 00 a6 ed 00 00 03 00 00 a0 3f ff ff 00 00 00 00 00 00 00 00 64 6f b6 08 01 00 00 00", }, Fixture { name: "double", kind: MxValueKind::Float64, value: MxValue::Float64(1.125), hex: "37 01 00 05 00 36 d7 02 00 9d 00 0a 00 1e 95 00 00 04 00 00 00 00 00 00 f2 3f ff ff 00 00 00 00 00 00 00 00 bf 04 b7 08 01 00 00 00", }, Fixture { name: "string", kind: MxValueKind::String, value: MxValue::String("AlphaMX".to_string()), hex: "37 01 00 05 00 36 d7 02 00 9e 00 0a 00 1a 94 00 00 05 14 00 00 00 10 00 00 00 41 00 6c 00 70 00 68 00 61 00 4d 00 58 00 00 00 ff ff 00 00 00 00 00 00 00 00 3d a1 b7 08 01 00 00 00", }, Fixture { // .NET formats DateTime via `M/d/yyyy h:mm:ss tt` InvariantCulture. // `new DateTime(2026, 4, 25, 2, 30, 0)` → "4/25/2026 2:30:00 AM". // Wire kind collapses to 0x05 String per `NmxWriteMessage.cs:107`. name: "datetime (M/d/yyyy h:mm:ss tt)", kind: MxValueKind::DateTime, value: MxValue::String("4/25/2026 2:30:00 AM".to_string()), hex: "37 01 00 05 00 36 d7 02 00 9f 00 0a 00 62 49 00 00 05 2e 00 00 00 2a 00 00 00 34 00 2f 00 32 00 35 00 2f 00 32 00 30 00 32 00 36 00 20 00 32 00 3a 00 33 00 30 00 3a 00 30 00 30 00 20 00 41 00 4d 00 00 00 ff ff 00 00 00 00 00 00 00 00 58 94 b8 08 01 00 00 00", }, // -------- Timestamped Write (Write2). Suffix `i16` is `0` (not -1) // and the 8-byte filler at suffix offsets 2..10 is replaced // by the FILETIME — see `design/40-protocol-invariants.md` // for the layout. `with_value` preserves the FILETIME from // the captured suffix, so the parity check is uniform. Fixture { name: "write2 int timestamp", kind: MxValueKind::Int32, value: MxValue::Int32(114), hex: "37 01 00 05 00 36 d7 02 00 9b 00 0a 00 3e da 00 00 02 72 00 00 00 00 00 00 72 68 3a 83 d4 dc 01 20 2e d8 08 01 00 00 00", }, Fixture { name: "write2 string timestamp", kind: MxValueKind::String, value: MxValue::String("Write2Alpha".to_string()), hex: "37 01 00 05 00 36 d7 02 00 9e 00 0a 00 1a 94 00 00 05 1c 00 00 00 18 00 00 00 57 00 72 00 69 00 74 00 65 00 32 00 41 00 6c 00 70 00 68 00 61 00 00 00 00 00 80 68 d5 9c ab d4 dc 01 9f 6e a6 0b 01 00 00 00", }, // -------- Arrays -------- Fixture { name: "int[]", kind: MxValueKind::Int32Array, value: MxValue::Int32Array(vec![201, 202, 203, 204, 205, 206, 207, 208, 209, 210]), hex: "37 01 00 05 00 36 d7 02 00 a4 00 0a 00 60 57 ff ff 42 00 00 00 00 0a 00 04 00 00 00 c9 00 00 00 ca 00 00 00 cb 00 00 00 cc 00 00 00 cd 00 00 00 ce 00 00 00 cf 00 00 00 d0 00 00 00 d1 00 00 00 d2 00 00 00 ff ff 00 00 00 00 00 00 00 00 4d c3 c4 08 01 00 00 00", }, Fixture { name: "bool[]", kind: MxValueKind::BoolArray, value: MxValue::BoolArray(vec![ true, true, false, false, true, true, false, false, true, true, ]), hex: "37 01 00 05 00 36 d7 02 00 a0 00 0a 00 fa 89 ff ff 41 00 00 00 00 0a 00 02 00 00 00 ff ff ff ff 00 00 00 00 ff ff ff ff 00 00 00 00 ff ff ff ff ff ff 00 00 00 00 00 00 00 00 a8 7f c5 08 01 00 00 00", }, Fixture { name: "float[]", kind: MxValueKind::Float32Array, value: MxValue::Float32Array(vec![ 1.25, 2.5, 3.75, 4.25, 5.5, 6.75, 7.25, 8.5, 9.75, 10.25, ]), hex: "37 01 00 05 00 36 d7 02 00 a3 00 0a 00 95 f1 ff ff 43 00 00 00 00 0a 00 04 00 00 00 00 00 a0 3f 00 00 20 40 00 00 70 40 00 00 88 40 00 00 b0 40 00 00 d8 40 00 00 e8 40 00 00 08 41 00 00 1c 41 00 00 24 41 ff ff 00 00 00 00 00 00 00 00 0c f4 c5 08 01 00 00 00", }, Fixture { name: "string[]", kind: MxValueKind::StringArray, value: MxValue::StringArray(vec![ "A01".to_string(), "B02".to_string(), "C03".to_string(), "D04".to_string(), "E05".to_string(), "F06".to_string(), "G07".to_string(), "H08".to_string(), "I09".to_string(), "J10".to_string(), ]), hex: "37 01 00 05 00 36 d7 02 00 a5 00 0a 00 5d 4b ff ff 45 00 00 00 00 0a 00 04 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 41 00 30 00 31 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 42 00 30 00 32 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 43 00 30 00 33 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 44 00 30 00 34 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 45 00 30 00 35 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 46 00 30 00 36 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 47 00 30 00 37 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 48 00 30 00 38 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 49 00 30 00 39 00 00 00 11 00 00 00 05 0c 00 00 00 08 00 00 00 4a 00 31 00 30 00 00 00 ff ff 00 00 00 00 00 00 00 00 c3 da c6 08 01 00 00 00", }, // -------- Timestamped arrays -------- Fixture { name: "write2 int[] timestamp", kind: MxValueKind::Int32Array, value: MxValue::Int32Array(vec![301, 302, 303, 304, 305, 306, 307, 308, 309, 310]), hex: "37 01 00 05 00 36 d7 02 00 a4 00 0a 00 60 57 ff ff 42 00 00 00 00 0a 00 04 00 00 00 2d 01 00 00 2e 01 00 00 2f 01 00 00 30 01 00 00 31 01 00 00 32 01 00 00 33 01 00 00 34 01 00 00 35 01 00 00 36 01 00 00 00 00 80 93 fc 76 ac d4 dc 01 43 35 ac 0b 01 00 00 00", }, ] } #[test] fn write_bodies_round_trip_byte_identical_against_dotnet_reference() { let mut failures: Vec = Vec::new(); for fx in write_body_fixtures() { let observed = hex_to_bytes(fx.hex); let template = match ObservedWriteBodyTemplate::from_observed(fx.kind, &observed) { Ok(t) => t, Err(e) => { failures.push(format!( "fixture {:?} from_observed errored: {e:?}", fx.name )); continue; } }; // .NET helper passes `writeIndex: 1` uniformly (Program.cs:128). let regen = match template.with_value(&fx.value, 1) { Ok(b) => b, Err(e) => { failures.push(format!("fixture {:?} with_value errored: {e:?}", fx.name)); continue; } }; if regen != observed { // Find first divergent byte for fast triage. let len = regen.len().min(observed.len()); let first = (0..len).find(|i| regen[*i] != observed[*i]); failures.push(format!( "fixture {:?} diverged: rust_len={} dotnet_len={} first_diff_at={:?}", fx.name, regen.len(), observed.len(), first, )); } } if !failures.is_empty() { panic!( "{} fixture(s) failed parity vs .NET reference:\n {}", failures.len(), failures.join("\n ") ); } } // ---- Transfer envelope round-trip ---------------------------------------- // // Mirrors `RunTransferEnvelopeRoundTrip` (Program.cs:174-182) and the canonical // fixture at Program.cs:102-104. The envelope template captures the 46-byte // header verbatim and re-emits it byte-identical given the same inner body. #[test] fn transfer_envelope_round_trips_byte_identical() { // Inner body and full transfer body — taken verbatim from // src/MxNativeCodec.Tests/Program.cs:102-104. let inner_hex = "37 01 00 05 00 36 d7 02 00 9b 00 0a 00 3e da 00 00 02 6d 00 00 00 ff ff 00 00 00 00 00 00 00 00 c9 14 b1 08 01 00 00 00"; let transfer_hex = "01 00 28 00 00 00 00 00 00 00 03 00 00 00 01 00 00 00 01 00 00 00 fb 7f 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 02 00 00 30 75 00 00 \ 37 01 00 05 00 36 d7 02 00 9b 00 0a 00 3e da 00 00 02 6d 00 00 00 ff ff 00 00 00 00 00 00 00 00 c9 14 b1 08 01 00 00 00"; let inner = hex_to_bytes(inner_hex); let transfer = hex_to_bytes(transfer_hex); let template = NmxTransferEnvelopeTemplate::from_observed(&transfer) .expect("envelope template parse failed"); // Decode inner — should match `inner_hex` byte-for-byte. let decoded_inner = template .decode_inner(&transfer) .expect("decode_inner errored"); assert_eq!( decoded_inner, inner.as_slice(), "decoded inner body diverged from canonical hex" ); // Re-encode the full 46+inner buffer — should match `transfer_hex`. let regen = template.encode(&inner); assert_eq!( regen, transfer, "re-encoded transfer body diverged from canonical hex" ); }