[M5] mxaccess-asb-nettcp/asb: xmlns raw-string + xsi/xsd on Body

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 <id>` 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 `<s:Body>`**. 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 `<s:Body>` 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
  `<s:Header>` section (Action / ConnectionValidator / MessageID
  via UniqueIdText / ReplyTo / To).
* `<s:Body>` xmlns:xsi + xmlns:xsd declarations match .NET.
* `<ConnectRequest>` opens identically.
* `<ConnectionId>` / `<ConsumerPublicKey>` / `<Data>` element names
  match.
* The only known remaining diff in the request: .NET emits
  `xmlns="http://asb.contracts.data/20111111"` on the inner
  `<Data>` 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) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-05 15:57:31 -04:00
parent c2222b16b0
commit 4c4177050c
2 changed files with 66 additions and 20 deletions
+52 -19
View File
@@ -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")?;
+14 -1
View File
@@ -300,11 +300,24 @@ pub fn encode_envelope(
tokens.push(NbfxToken::EndElement); // </s:Header>
// <s:Body>
// <s:Body xmlns:xsi="...XMLSchema-instance" xmlns:xsd="...XMLSchema">
// 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); // </s:Body>