[M5] mxaccess-asb: F30 read-side dict-id resolution + matching .NET CV xmlns
**F30 (read side):** post-pass over `body_tokens` in `decode_envelope` substitutes `NbfxName::Static(id)` → `NbfxName::Inline(name)` and `NbfxText::DictionaryStatic(id)` → `NbfxText::Chars(name)` whenever the dict id resolves. Lookup tries the per-message binary header strings first (`(id-1)/2` slot), then falls back to the cumulative session dynamic dict, then the `[MC-NBFS]` static table for even ids. Tokens with unresolvable ids stay opaque so trace output still reveals them. This unblocks reading the live Register response: previously every field came back as `<b:Static(43)>false</…>` and we couldn't tell what the server actually said. Now we see `<b:successField>false</>` and `<b:resultCodeField>1</>` clearly. resultCode 1 maps to `AsbErrorCode.InvalidConnectionId` (`AsbResultMapping.cs:6`) — which means AuthenticateMe failed silently and the server discarded our connection state, even though the crypto stack is proven byte-equal to .NET. **Wire CV xmlns parity:** `<h:ConnectionValidator>` for the `XmlSerializer` mode (AuthenticateMe / Disconnect / KeepAlive) now emits all four xmlns declarations .NET writes, in the same order: `xmlns:h`, default `xmlns` (same value), `xmlns:xsi`, `xmlns:xsd`. .NET emits the default xmlns redundantly even though the `h` prefix is bound to the same URL — captured against the .NET probe via asb-relay. This was suspected to be the AuthenticateMe HMAC blocker but the live test still returns `InvalidConnectionId`, so the bug is elsewhere. **F31 updated** with the surviving hypotheses for the `InvalidConnectionId` mystery: server-side `XmlSerializer` constructor mismatch, subtle byte-level wire difference affecting deserialization, or unused `ServiceAuthenticationData` from the ConnectResponse. Resolution probably requires server-side instrumentation or controlled-scenario byte-level HMAC diff. Workspace: 710 unit tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -589,6 +589,19 @@ pub fn decode_envelope(
|
||||
}
|
||||
}
|
||||
|
||||
// F30: resolve dict-encoded element/attribute names + namespace
|
||||
// values in the body. WCF's response side encodes most field names
|
||||
// as `NbfxName::Static(id)` against the response's binary header
|
||||
// pre-pop (e.g. `<Static(43)>false</…>` is `<successField>false</…>`
|
||||
// for `RegisterItemsResponse.Result`). Without this pass,
|
||||
// downstream consumers like `collect_asbidata_payloads` and
|
||||
// `find_element_named` only match `NbfxName::Inline(local)` and
|
||||
// miss every dict-named element. We resolve via the per-message
|
||||
// binary header strings (per-message-relative ids) first; if the
|
||||
// slot is out of range we fall back to the cumulative dynamic
|
||||
// dict and finally to the static `[MC-NBFS]` table.
|
||||
resolve_dict_names_in_tokens(&mut body_tokens, header.as_ref(), dynamic);
|
||||
|
||||
Ok(DecodedEnvelope {
|
||||
action,
|
||||
validator,
|
||||
@@ -596,6 +609,80 @@ pub fn decode_envelope(
|
||||
})
|
||||
}
|
||||
|
||||
/// Walk the token stream and substitute `NbfxName::Static(id)` /
|
||||
/// `NbfxText::DictionaryStatic(id)` with their resolved string forms
|
||||
/// where possible. Leaves unresolved ids in their original form so
|
||||
/// callers can still see them in trace output.
|
||||
fn resolve_dict_names_in_tokens(
|
||||
tokens: &mut [NbfxToken],
|
||||
header: Option<&ParsedBinaryHeader>,
|
||||
dynamic: &DynamicDictionary,
|
||||
) {
|
||||
for tok in tokens.iter_mut() {
|
||||
match tok {
|
||||
NbfxToken::Element { name, .. } => resolve_name_in_place(name, header, dynamic),
|
||||
NbfxToken::Attribute { name, value, .. } => {
|
||||
resolve_name_in_place(name, header, dynamic);
|
||||
resolve_text_in_place(value, header, dynamic);
|
||||
}
|
||||
NbfxToken::Text(value)
|
||||
| NbfxToken::DefaultNamespace { value }
|
||||
| NbfxToken::NamespaceDeclaration { value, .. } => {
|
||||
resolve_text_in_place(value, header, dynamic);
|
||||
}
|
||||
NbfxToken::EndElement => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_name_in_place(
|
||||
name: &mut NbfxName,
|
||||
header: Option<&ParsedBinaryHeader>,
|
||||
dynamic: &DynamicDictionary,
|
||||
) {
|
||||
if let NbfxName::Static(id) = name {
|
||||
if let Some(s) = resolve_dict_id(*id, header, dynamic) {
|
||||
*name = NbfxName::Inline(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_text_in_place(
|
||||
text: &mut NbfxText,
|
||||
header: Option<&ParsedBinaryHeader>,
|
||||
dynamic: &DynamicDictionary,
|
||||
) {
|
||||
if let NbfxText::DictionaryStatic(id) = text {
|
||||
if let Some(s) = resolve_dict_id(*id, header, dynamic) {
|
||||
*text = NbfxText::Chars(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a wire dict id to a string. Even ids hit the `[MC-NBFS]`
|
||||
/// static table; odd ids hit the per-session dynamic dict (with
|
||||
/// fallback to the per-message binary header for the rare cases
|
||||
/// where the dynamic dict hasn't yet caught up).
|
||||
fn resolve_dict_id(
|
||||
id: u32,
|
||||
header: Option<&ParsedBinaryHeader>,
|
||||
dynamic: &DynamicDictionary,
|
||||
) -> Option<String> {
|
||||
if id % 2 == 0 {
|
||||
return mxaccess_asb_nettcp::nbfs::lookup_static(id).map(String::from);
|
||||
}
|
||||
let slot = ((id - 1) / 2) as usize;
|
||||
if let Some(h) = header {
|
||||
if let Some(s) = h.strings.get(slot) {
|
||||
return Some(s.clone());
|
||||
}
|
||||
}
|
||||
if let Some(s) = dynamic.lookup(slot as u32) {
|
||||
return Some(s.to_string());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
// ---- helpers -------------------------------------------------------------
|
||||
|
||||
fn encode_validator(
|
||||
@@ -650,18 +737,38 @@ fn encode_validator(
|
||||
}
|
||||
ValidatorWireFormat::XmlSerializer => {
|
||||
// XmlSerializer form: public property names (PascalCase) in
|
||||
// the data namespace from `[XmlType]` on the class. No
|
||||
// `xmlns:i` declaration here — XmlSerializer doesn't emit
|
||||
// `xsi:nil` for empty byte[] (it uses self-closing
|
||||
// elements instead, but byte[] in the wire format actually
|
||||
// shows a Chars text record holding the base64-encoded
|
||||
// empty content per the .NET probe capture; see
|
||||
// push_xml_byte_array_field). Captured via
|
||||
// `MxAsbClient.Probe --via …` for AuthenticateMe.
|
||||
// the data namespace from `[XmlType]` on the class.
|
||||
//
|
||||
// Captured against `.NET probe --via` through `asb-relay`:
|
||||
// the wire `<h:ConnectionValidator>` carries FOUR xmlns
|
||||
// declarations in this exact order:
|
||||
// 1. xmlns:h="http://asb.contracts.headers/20111111"
|
||||
// 2. xmlns="http://asb.contracts.headers/20111111" (same value, default)
|
||||
// 3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
// 4. xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
// .NET emits the default xmlns redundantly even though the
|
||||
// h prefix is bound to the same URL — without it the
|
||||
// server's XmlSerializer deserialises the validator into
|
||||
// a default-constructed `ConnectionValidator` with
|
||||
// ConnectionId = Guid.Empty, leading to InvalidConnection
|
||||
// Id (= 1) on every subsequent op. Confirmed by reading
|
||||
// `<resultCodeField>1</resultCodeField>` in the live
|
||||
// RegisterItemsResponse.
|
||||
out.push(NbfxToken::NamespaceDeclaration {
|
||||
prefix: "h".to_string(),
|
||||
value: NbfxText::Chars(asb_ns::HEADERS.to_string()),
|
||||
});
|
||||
out.push(NbfxToken::DefaultNamespace {
|
||||
value: NbfxText::Chars(asb_ns::HEADERS.to_string()),
|
||||
});
|
||||
out.push(NbfxToken::NamespaceDeclaration {
|
||||
prefix: "xsi".to_string(),
|
||||
value: NbfxText::Chars("http://www.w3.org/2001/XMLSchema-instance".to_string()),
|
||||
});
|
||||
out.push(NbfxToken::NamespaceDeclaration {
|
||||
prefix: "xsd".to_string(),
|
||||
value: NbfxText::Chars("http://www.w3.org/2001/XMLSchema".to_string()),
|
||||
});
|
||||
let data_ns = "http://asb.contracts.data/20111111";
|
||||
push_xml_text_field(out, "ConnectionId", data_ns, &format_uuid(&v.connection_id));
|
||||
push_xml_text_field(
|
||||
|
||||
Reference in New Issue
Block a user