From 4c4177050c37e04d16d912cf27a5bded4019deb7 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 5 May 2026 15:57:31 -0400 Subject: [PATCH] [M5] mxaccess-asb-nettcp/asb: xmlns raw-string + xsi/xsd on Body MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WIRE-FORMAT BREAKTHROUGH — our envelope is now valid NBFX/WCF. ConnectRequest reaches the server's operation handler. Direct-port-808 Connect now returns a server-side fault (operation invocation error from the placeholder DH key) rather than TCP RST. With a real DH key, asb-subscribe gets all the way to "response is missing required field ServicePublicKey/Data" — meaning our Connect request processed end-to-end, the server returned a ConnectResponse, and the only remaining issue is in our response-body decoder. Fixes: 1. **`XmlnsAttribute` (0x09) value is a RAW length-prefixed string, not a text record**. Per `[MC-NBFX]` §2.2.3, xmlns attribute values are `[length][bytes]`, NOT `[text-record-byte][value]`. Our F21 was emitting `aa ` for dict-static values which the receiver misparsed as a 0xAA-length string. Same fix applies to `ShortXmlnsAttribute` (0x08). Encoder now picks raw-string for `Chars` value, raw-int31 (via 0x0B) for `DictionaryStatic` value; decoder reads raw string in both code paths. 2. **xmlns:xsi + xmlns:xsd on ``**. WCF declares these namespaces on Body before opening the operation request element. Our envelope encoder now emits both as raw-string xmlns attrs right after `` opens. Required for `xsi:type` annotations that appear inside DataContract-serialised body fields. Combined wire-byte impact (verified via asb-relay side-by-side diff): * All header bytes match .NET byte-for-byte through the entire `` section (Action / ConnectionValidator / MessageID via UniqueIdText / ReplyTo / To). * `` xmlns:xsi + xmlns:xsd declarations match .NET. * `` opens identically. * `` / `` / `` element names match. * The only known remaining diff in the request: .NET emits `xmlns="http://asb.contracts.data/20111111"` on the inner `` element (the PublicKey class's XmlType namespace); we don't. Likely an issue but apparently non-fatal — the server processed our request successfully past this point. Live status: * Direct port-808 connect with real DH key: server returns "response is missing required field ServicePublicKey/Data" — meaning we sent a valid Connect, server replied with a ConnectResponse, but our decoder can't find the field. Next iteration is response-side decode work. Workspace: 702 tests pass; clippy + fmt clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- rust/crates/mxaccess-asb-nettcp/src/nbfx.rs | 71 +++++++++++++++------ rust/crates/mxaccess-asb/src/envelope.rs | 15 ++++- 2 files changed, 66 insertions(+), 20 deletions(-) diff --git a/rust/crates/mxaccess-asb-nettcp/src/nbfx.rs b/rust/crates/mxaccess-asb-nettcp/src/nbfx.rs index 2efd3a2..13d5de5 100644 --- a/rust/crates/mxaccess-asb-nettcp/src/nbfx.rs +++ b/rust/crates/mxaccess-asb-nettcp/src/nbfx.rs @@ -340,24 +340,50 @@ fn encode_one( value, } => encode_attribute(prefix.as_deref(), name, value, dynamic, out), NbfxToken::DefaultNamespace { value } => { - out.push(REC_SHORT_XMLNS_ATTRIBUTE); - encode_text_string_or_dict(value, dynamic, out) + // Per `[MC-NBFX]` §2.2.3: `ShortXmlnsAttribute` (0x08) + // value is a RAW length-prefixed string (same convention + // as 0x09 — see comment on NamespaceDeclaration below). + match value { + NbfxText::Chars(s) => { + out.push(REC_SHORT_XMLNS_ATTRIBUTE); + encode_string(s.as_bytes(), out)?; + Ok(()) + } + _ => Err(NbfxError::InvalidUtf8 { + stage: "default-xmlns-must-be-Chars", + }), + } } NbfxToken::NamespaceDeclaration { prefix, value } => { - // WCF emits `DictionaryXmlnsAttribute` (0x0B) when the - // value is a static-dictionary id, and `XmlnsAttribute` - // (0x09) when it's an inline string. Stricter parsers - // reject the long form when a dict-id is available. - if let NbfxText::DictionaryStatic(id) = value { - out.push(REC_DICT_XMLNS_ATTRIBUTE); - encode_string(prefix.as_bytes(), out)?; - encode_multibyte_int31_to_nbfx(out, *id)?; - Ok(()) - } else { - out.push(REC_XMLNS_ATTRIBUTE); - encode_string(prefix.as_bytes(), out)?; - encode_text_string_or_dict(value, dynamic, out) + // Per `[MC-NBFX]` §2.2.3: `XmlnsAttribute` (0x09) value is + // a RAW length-prefixed string — NOT a text record like + // regular Attribute values. `DictionaryXmlnsAttribute` + // (0x0B) value is a raw multibyte-int31 dict id. Either + // form omits the text-record byte and width-tag. + // + // We pick 0x0B when the value is `DictionaryStatic(id)` + // (matches WCF's encoding for SOAP/WS-Addressing names), + // and 0x09 + raw-string for `Chars(s)` (matches WCF's + // encoding for namespaces not in the static dict — e.g. + // xsi/xsd, or operation-specific URIs). + match value { + NbfxText::DictionaryStatic(id) => { + out.push(REC_DICT_XMLNS_ATTRIBUTE); + encode_string(prefix.as_bytes(), out)?; + encode_multibyte_int31_to_nbfx(out, *id)?; + } + NbfxText::Chars(s) => { + out.push(REC_XMLNS_ATTRIBUTE); + encode_string(prefix.as_bytes(), out)?; + encode_string(s.as_bytes(), out)?; + } + _ => { + return Err(NbfxError::InvalidUtf8 { + stage: "xmlns-value-must-be-Chars-or-DictionaryStatic", + }); + } } + Ok(()) } NbfxToken::Text(text) => encode_text(text, with_end, out), } @@ -672,13 +698,20 @@ pub fn decode_tokens( }); } REC_SHORT_XMLNS_ATTRIBUTE => { - let value = decode_text_record(input, &mut cursor)?; - tokens.push(NbfxToken::DefaultNamespace { value }); + let value_str = decode_string(input, &mut cursor, "default-xmlns-value")?; + tokens.push(NbfxToken::DefaultNamespace { + value: NbfxText::Chars(value_str), + }); } REC_XMLNS_ATTRIBUTE => { + // Per spec, value is a raw length-prefixed string, + // NOT a text record. let prefix = decode_string(input, &mut cursor, "xmlns-prefix")?; - let value = decode_text_record(input, &mut cursor)?; - tokens.push(NbfxToken::NamespaceDeclaration { prefix, value }); + let value_str = decode_string(input, &mut cursor, "xmlns-value")?; + tokens.push(NbfxToken::NamespaceDeclaration { + prefix, + value: NbfxText::Chars(value_str), + }); } REC_DICT_XMLNS_ATTRIBUTE => { let prefix = decode_string(input, &mut cursor, "dict-xmlns-prefix")?; diff --git a/rust/crates/mxaccess-asb/src/envelope.rs b/rust/crates/mxaccess-asb/src/envelope.rs index ae54845..ed85c09 100644 --- a/rust/crates/mxaccess-asb/src/envelope.rs +++ b/rust/crates/mxaccess-asb/src/envelope.rs @@ -300,11 +300,24 @@ pub fn encode_envelope( tokens.push(NbfxToken::EndElement); // - // + // + // WCF emits xmlns:xsi + xmlns:xsd as raw-string xmlns attributes on + // the Body element (verified against .NET probe wire capture). + // These declarations are required for `xsi:type` annotations that + // appear inside DataContract-serialised body fields (e.g. + // ConnectRequest's nested PublicKey). tokens.push(NbfxToken::Element { prefix: Some("s".to_string()), name: NbfxName::Static(ns::BODY), }); + tokens.push(NbfxToken::NamespaceDeclaration { + prefix: "xsi".to_string(), + value: NbfxText::Chars("http://www.w3.org/2001/XMLSchema-instance".to_string()), + }); + tokens.push(NbfxToken::NamespaceDeclaration { + prefix: "xsd".to_string(), + value: NbfxText::Chars("http://www.w3.org/2001/XMLSchema".to_string()), + }); tokens.extend_from_slice(&envelope.body_tokens); tokens.push(NbfxToken::EndElement); //