From b66f5bb01899fc47fe9399302715e8bede63bf02 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 6 May 2026 02:58:25 -0400 Subject: [PATCH] [F34 evidence] capture AddMonitoredItems request wire + decoder trace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Investigation continued via examples/asb-relay.rs middleman: captured the .NET probe's verbatim AddMonitoredItems request bytes (695 bytes with the 3-byte NMF SizedEnvelope header). Saved at rust/crates/mxaccess-asb/tests/fixtures/add-monitored-items-request-wire.bin as the ground-truth shape MxDataProvider actually accepts. New tests/add_monitored_items_request_capture.rs runs decode_envelope over the capture and dumps every NBFX token to stderr for inspection. Decoded trace surfaces a SECOND, deeper issue: The F30 dynamic-dict-resolution post-pass at envelope.rs::resolve_dict_names_in_tokens mis-maps per-session dict ids. Decoding the captured request renders namespace-URL slots as field-name strings: body[1]=DefaultNamespace { value: Chars("nameField") } ← bogus body[7]=NamespaceDeclaration { prefix: "i", value: Chars("activeField") } ← bogus and leaves most element names as `Static(NN)` instead of resolving to inline names like `activeField` / `bufferedField` / `itemField`. This blocks F34's substantive fix (rewrite build_add_monitored_items_request_body to use DataContract field-suffix names matching the wire). We can't validate the rewritten builder against the captured fixture until the dict post-pass produces the right strings. design/followups.md F34 updated with two-prerequisite resolution plan: 1. Fix the F30 dynamic-dict resolution so the captured request decodes to recognisable inline names. 2. Rewrite the AddMonitoredItems / DeleteMonitoredItems builders against the now-readable structure (DataContract field names + namespace prefixes for ASBIDataV2Contract / ASBContract + nested DataContract serialization of ItemIdentity inside `` and Variants inside userDataField / valueDeadbandField). Workspace: mxaccess-asb 96 → 97 (+1 capture-driven analysis test); default-feature clippy clean. The HMAC canonical-XML signing path remains correct (F28 fixtures are byte-equal to .NET); only the binary NBFX wire body needs the rewrite. Co-Authored-By: Claude Opus 4.7 (1M context) --- design/followups.md | 7 ++- .../add_monitored_items_request_capture.rs | 47 ++++++++++++++++++ .../add-monitored-items-request-wire.bin | Bin 0 -> 695 bytes 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 rust/crates/mxaccess-asb/tests/add_monitored_items_request_capture.rs create mode 100644 rust/crates/mxaccess-asb/tests/fixtures/add-monitored-items-request-wire.bin diff --git a/design/followups.md b/design/followups.md index 975ef4c..33f50a1 100644 --- a/design/followups.md +++ b/design/followups.md @@ -90,7 +90,12 @@ For the per-step body of every line listed in the cumulative execution log, see For ops where the body is purely `IAsbCustomSerializableType` arrays (Read, Register, Unregister), no DataContract names appear — every payload is wrapped as `{bytes}` (binary fast-path) and our builders are correct. The DataContract schema only matters for ops carrying non-`IAsbCustomSerializable` types like `MonitoredItem` and `WriteValue`. -**Resolves when:** `build_add_monitored_items_request_body` and `build_delete_monitored_items_request_body` rewrite each `MonitoredItem` child as the DataContract field-suffix names (`activeField` instead of `Active`, etc.), with the `*Specified` siblings emitted as their own elements (`activeFieldSpecified`, `idFieldSpecified`, etc.). The dictionary-id pre-population that .NET's WCF binary writer uses to compress these long strings is a perf optimisation; an inline-string emit will work for correctness. Likely the same fix applies to `WriteBasicRequest`'s `WriteValue[]? Values` field (also non-`IAsbCustomSerializable`) — that's a future capture-and-verify pass. +**Resolves when:** Two prerequisites: + +1. **F30 dynamic-dict resolution bug** — captured `tests/fixtures/add-monitored-items-request-wire.bin` (the .NET probe's verbatim 695-byte AddMonitoredItems request via `examples/asb-relay.rs`), decoded via `decode_envelope` at `tests/add_monitored_items_request_capture.rs`. The trace shows `DefaultNamespace { value: Chars("nameField") }` and `NamespaceDeclaration { prefix: "i", value: Chars("activeField") }` — namespace URL slots resolved to field-name strings, plus most element names left as `Static(NN)` instead of resolving to inline names. The F30 cumulative dynamic-dict post-pass at `envelope.rs::resolve_dict_names_in_tokens` mis-maps per-session dynamic dict ids; the fix needs reproducing exactly which dict each id refers to (per-message header vs cumulative dynamic vs `[MC-NBFS]` static) and resolving in the right order. +2. **Builder rewrite** — once (1) lands and we can read the captured request structurally, rewrite `build_add_monitored_items_request_body` and `build_delete_monitored_items_request_body` to emit each `MonitoredItem` child as the DataContract field-suffix names (`activeField` / `activeFieldSpecified` / `bufferedField` / `itemField` / `sampleIntervalField` / `timeDeadbandField` / `timeDeadbandFieldSpecified` / `userDataField` / `valueDeadbandField`) under a `b` namespace prefix that maps to `http://schemas.datacontract.org/2004/07/ArchestrAServices.ASBIDataV2Contract`. The nested `` carries an ItemIdentity serialized via DataContract (NOT the binary `` fast-path — that only kicks in at the outer body-member level) with children `contextNameField` / `idField` / `idFieldSpecified` / `nameField` / `referenceTypeField` / `typeField` under a different `b` prefix mapping to `http://schemas.datacontract.org/2004/07/ArchestrAServices.ASBContract`. The Variant fields (`userDataField` / `valueDeadbandField`) carry `lengthField` / `payloadField` / `typeField` children. Same fix likely applies to `WriteBasicRequest`'s `WriteValue[]? Values` field (also non-`IAsbCustomSerializable`); needs its own capture-and-verify pass. + +The dictionary-id pre-population that .NET's WCF binary writer uses is a perf optimisation; an inline-string emit will work for correctness once the structure is right. **Bonus context discovered while debugging F34:** - `MinimalMonitoredItem` gained an `active: Option` field with a `with_active(item, interval, active)` constructor. Without `true` on the wire, MxDataProvider treats the subscription as inactive even when AddMonitoredItems "succeeds" — F26 stream then never sees values. (Once the field-name fix lands, this becomes the determining factor for whether values flow.) diff --git a/rust/crates/mxaccess-asb/tests/add_monitored_items_request_capture.rs b/rust/crates/mxaccess-asb/tests/add_monitored_items_request_capture.rs new file mode 100644 index 0000000..165b92e --- /dev/null +++ b/rust/crates/mxaccess-asb/tests/add_monitored_items_request_capture.rs @@ -0,0 +1,47 @@ +//! F34 — wire-byte trace of a captured `AddMonitoredItemsRequest`. +//! +//! `tests/fixtures/add-monitored-items-request-wire.bin` is the +//! verbatim C→S bytes the .NET probe (`MxAsbClient.Probe --subscribe +//! --via=net.tcp://127.0.0.1:8088/...`) sent to MxDataProvider on +//! 2026-05-06. The exchange led to a working subscription that +//! delivered values; this is the request shape MxDataProvider +//! actually accepts. +//! +//! Test goal: dump every NBFX token in the body so we can read off +//! the exact element-name shape (DataContract field-suffix names per +//! `[DataMember(Name=...)]`, NOT XmlSerializer property names) and +//! re-implement `build_add_monitored_items_request_body` against it. +//! +//! Frame layout: 3-byte NMF SizedEnvelope header (`06 b4 05`, +//! varint length = 692) + 692-byte SOAP envelope. + +#![allow( + clippy::unwrap_used, + clippy::expect_used, + clippy::indexing_slicing, + clippy::panic +)] + +use mxaccess_asb::decode_envelope; +use mxaccess_asb_nettcp::nbfx::DynamicDictionary; + +#[test] +fn add_monitored_items_request_capture_decoder_trace() { + let raw = std::fs::read( + std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/add-monitored-items-request-wire.bin"), + ) + .expect("read fixture"); + assert_eq!(raw.len(), 695, "frame length sanity check"); + + // Strip 3-byte NMF SizedEnvelope header. + let envelope = &raw[3..]; + assert_eq!(envelope.len(), 692); + + let mut dict = DynamicDictionary::new(); + let decoded = decode_envelope(envelope, &mut dict).expect("decode_envelope succeeds"); + eprintln!("=== body tokens ({} total) ===", decoded.body_tokens.len()); + for (i, tok) in decoded.body_tokens.iter().enumerate() { + eprintln!(" body[{i}]={tok:?}"); + } +} diff --git a/rust/crates/mxaccess-asb/tests/fixtures/add-monitored-items-request-wire.bin b/rust/crates/mxaccess-asb/tests/fixtures/add-monitored-items-request-wire.bin new file mode 100644 index 0000000000000000000000000000000000000000..47c51c283bfaa25866b746d640756eac58366fd6 GIT binary patch literal 695 zcmZXR&u-H|5XQ5#4NcsXrYR~af)hdrk$9ayid=+H950m>LIo+QdR*J%W|3nXdlS)P zKvBej2goy2hzqFCz?GNbSk`u05jgF)qi=rmX(s!9?Z?W)0q4QKEVp`E#n36I{pvoo z?XK@J?uTF-9EOqM-D&+>djs!B5b@34$c(Iz1)TYwVXr0rmkVfQ4PZ#4iY>U7@9~gY zyyAzaauwscjGJ;R6po08tsaD9WZSC(|0>sW3Zn?Zc;!i}P3 z3DInSUC-aod^|6AauOOy5@H4IR=zlYphC6UaIjg%)KtoKMODkHQm>U&TyvTVI2f#| zR>~KVc3ap{L)#S^>3d&)XL0H0!LRPc#W(*EUMKn!{qgw@(l_EUE2zJz$@#mQ@>$Yq z5;7a{MA9Vcj69u@XH)W=v=C`e2$_