//! Round-trip parity: buffered-subscribe `RegisterReference` (opcode `0x10`) //! body, captured live with Frida. //! //! Closes the F36 DoD bullet 6 (`design/followups.md`): "Round-trip fixture //! loaded from `captures/079-frida-add-buffered-advise-testint/` validating //! the wire-byte sequence (call → response)." //! //! The .NET reference's [`MxNativeSession.RegisterBufferedItemAsync`] //! (`MxNativeSession.cs:272-310`) builds a single `RegisterReference` frame //! with `item_definition` suffixed by `.property(buffer)` and //! `subscribe = true`. The Rust counterpart is //! [`mxaccess::Session::subscribe_buffered`], which composes //! [`mxaccess_codec::NmxReferenceRegistrationMessage::to_buffered_item_definition`] //! with [`mxaccess_codec::NmxReferenceRegistrationMessage::encode`]. //! //! Both fixtures below are the **inner LMX `RegisterReference` body** copied //! verbatim from the corresponding capture's //! `frida-events.tsv` (the `nmx.enter ... CNmxAdapter.PutRequest` row whose //! candidate body starts with `10 01 00 ...`): //! //! - `082-frida-add-buffered-plain-advise-testint`: 173-byte body for //! `(itemDefinition = "TestInt", itemContext = "TestChildObject")` with //! correlation id `fb df 86 dc 1f c4 34 4b bb 26 a9 97 35 e9 b7 57`. //! - `079-frida-add-buffered-advise-testint`: 173-byte body for the same //! `(itemDefinition, itemContext)` pair with correlation id //! `32 c3 d9 6d ed 72 f1 48 84 85 37 0c 66 bc f8 92`. (Capture 079 is the //! `add-buffered-advise` scenario, which exercises the same wire frame //! under a slightly different harness mode — both captures land on the //! same `RegisterReference` shape.) #![allow( clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing, clippy::panic )] use mxaccess_codec::NmxReferenceRegistrationMessage; /// Decode a space-separated hex string into bytes. Mirrors /// `Convert.FromHexString` from 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() } /// Captured `RegisterReference` (0x10) body from /// `captures/082-frida-add-buffered-plain-advise-testint/frida-events.tsv`, /// line 45 (`nmx.enter ... CNmxAdapter.PutRequest`, candidate size 173). const CAPTURE_082_BODY_HEX: &str = "\ 10 01 00 \ 01 00 00 00 \ fb df 86 dc 1f c4 34 4b bb 26 a9 97 35 e9 b7 57 \ ff ff \ 00 00 \ 01 00 00 00 \ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 \ 32 00 00 81 \ 54 00 65 00 73 00 74 00 49 00 6e 00 74 00 \ 2e 00 70 00 72 00 6f 00 70 00 65 00 72 00 74 00 79 00 \ 28 00 62 00 75 00 66 00 66 00 65 00 72 00 29 00 \ 00 00 \ 00 00 00 00 00 00 00 00 \ 20 00 00 00 \ 54 00 65 00 73 00 74 00 43 00 68 00 69 00 6c 00 64 00 \ 4f 00 62 00 6a 00 65 00 63 00 74 00 \ 00 00 \ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 \ 01"; /// Captured `RegisterReference` (0x10) body from /// `captures/079-frida-add-buffered-advise-testint/frida-events.tsv`, /// line 45 (`nmx.enter ... CNmxAdapter.PutRequest`, candidate size 173). /// Differs from `CAPTURE_082_BODY_HEX` only in the 16-byte correlation id — /// the rest of the wire shape is identical because both captures exercise /// the same `(itemDefinition="TestInt", itemContext="TestChildObject")` pair. const CAPTURE_079_BODY_HEX: &str = "\ 10 01 00 \ 01 00 00 00 \ 32 c3 d9 6d ed 72 f1 48 84 85 37 0c 66 bc f8 92 \ ff ff \ 00 00 \ 01 00 00 00 \ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 \ 32 00 00 81 \ 54 00 65 00 73 00 74 00 49 00 6e 00 74 00 \ 2e 00 70 00 72 00 6f 00 70 00 65 00 72 00 74 00 79 00 \ 28 00 62 00 75 00 66 00 66 00 65 00 72 00 29 00 \ 00 00 \ 00 00 00 00 00 00 00 00 \ 20 00 00 00 \ 54 00 65 00 73 00 74 00 43 00 68 00 69 00 6c 00 64 00 \ 4f 00 62 00 6a 00 65 00 63 00 74 00 \ 00 00 \ 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 \ 01"; /// Helper — assemble a `NmxReferenceRegistrationMessage` matching the /// captured fixture and assert it encodes to the same bytes the .NET /// reference + LMX server emit on the wire. Mirrors the .NET reference's /// `MxNativeSession.RegisterBufferedItemAsync` request build: /// /// ```csharp /// var message = new NmxReferenceRegistrationMessage( /// itemHandle, /// subscription.CorrelationId, /// NmxReferenceRegistrationMessage.ToBufferedItemDefinition(itemDefinition), /// itemContext, /// Subscribe: true); /// ``` fn assert_roundtrip(captured_hex: &str, correlation_id: [u8; 16]) { let captured = hex_to_bytes(captured_hex); // Parse the captured bytes — should succeed cleanly. let parsed = NmxReferenceRegistrationMessage::parse(&captured) .expect("parse captured RegisterReference body"); // Sanity-check the high-level fields the F36 implementation depends on. assert_eq!( parsed.item_handle, 1, "captured item_handle (LMXProxy harness uses sequential int handles starting at 1)" ); assert_eq!(parsed.item_correlation_id, correlation_id); assert_eq!( parsed.item_definition, "TestInt.property(buffer)", "buffered suffix preserved" ); assert!( parsed.subscribe, "subscribe flag — buffered RegisterReference always sets it (.NET MxNativeSession.cs:298)" ); // Re-encode and confirm byte-identical. let re_encoded = parsed.encode(); assert_eq!( re_encoded, captured, "RegisterReference body must round-trip byte-identical" ); // Also confirm the suffix helper is idempotent on an already-buffered name // — the .NET reference does the same case-insensitive guard at // `NmxReferenceRegistrationMessage.cs:96-102`. let resuffixed = NmxReferenceRegistrationMessage::to_buffered_item_definition(&parsed.item_definition) .expect("re-applying buffered suffix"); assert_eq!(resuffixed, parsed.item_definition); } #[test] fn capture_082_register_reference_round_trips() { assert_roundtrip( CAPTURE_082_BODY_HEX, [ 0xfb, 0xdf, 0x86, 0xdc, 0x1f, 0xc4, 0x34, 0x4b, 0xbb, 0x26, 0xa9, 0x97, 0x35, 0xe9, 0xb7, 0x57, ], ); } #[test] fn capture_079_register_reference_round_trips() { assert_roundtrip( CAPTURE_079_BODY_HEX, [ 0x32, 0xc3, 0xd9, 0x6d, 0xed, 0x72, 0xf1, 0x48, 0x84, 0x85, 0x37, 0x0c, 0x66, 0xbc, 0xf8, 0x92, ], ); } #[test] fn buffered_suffix_helper_matches_captured_definition() { // F36 DoD bullet 1 verification: the codec helper that the Rust // `Session::subscribe_buffered` calls must produce the exact suffix // the captured wire bytes carry. let suffixed = NmxReferenceRegistrationMessage::to_buffered_item_definition("TestInt").unwrap(); assert_eq!(suffixed, "TestInt.property(buffer)"); } #[test] fn buffered_register_reference_constructed_from_session_inputs_matches_capture_082() { // Forward-build the message from the same inputs `Session::subscribe_buffered` // gathers (correlation id + already-suffixed item definition + empty // item context, with subscribe=true) and assert the encoded body // matches the capture once we plug in the capture's specific // `(item_context = "TestChildObject")` from the .NET probe harness. // // The Rust simple-form `subscribe_buffered(reference, ...)` passes // the FULL reference as `item_definition` with empty `item_context`; // capture 082 came from the LMXProxy compatibility surface which // splits the reference into `(itemDefinition="TestInt", itemContext="TestChildObject")`. // Both forms are valid on the wire — this test exercises the // split-context form to confirm the Rust codec produces the identical // bytes the live LMX server saw. let captured = hex_to_bytes(CAPTURE_082_BODY_HEX); let item_definition = NmxReferenceRegistrationMessage::to_buffered_item_definition("TestInt").unwrap(); let msg = NmxReferenceRegistrationMessage { item_handle: 1, item_correlation_id: [ 0xfb, 0xdf, 0x86, 0xdc, 0x1f, 0xc4, 0x34, 0x4b, 0xbb, 0x26, 0xa9, 0x97, 0x35, 0xe9, 0xb7, 0x57, ], item_definition, item_context: "TestChildObject".to_string(), subscribe: true, reserved_25_27: [0; 2], reserved_31_55: [0; 24], }; let encoded = msg.encode(); assert_eq!(encoded, captured); }