[M5] mxaccess-asb-nettcp/asb: F21 short forms + EndElement fix + UniqueIdText

Three NBFX-spec corrections discovered by diffing our wire output
against the .NET probe's capture:

1. **EndElement is 0x01, NOT 0x00**. Our F21 had this wrong since the
   first iteration. Our round-trip tests passed because encode and
   decode used the same wrong value, but interop with WCF's parser
   silently failed (TCP RST on every request). Fixed by changing
   `REC_END_ELEMENT` to 0x01 — all 702 tests pass on the new value.

2. **Single-letter prefix short forms**. WCF uses
   `PrefixDictionaryElement_<a-z>` (records 0x44-0x5D) and
   `PrefixDictionaryAttribute_<a-z>` (records 0x0C-0x25) for
   single-character prefixes. Our F21 always used the long forms
   (0x43 prefix-string + dict-id, etc.). The encoder now emits the
   short form when the prefix is a single ASCII lowercase letter; the
   decoder accepts both. New `prefix_letter_offset(prefix)` helper.

3. **`DictionaryXmlnsAttribute` (0x0B)** for xmlns:prefix declarations
   whose value is a static-dict id. The long form (0x09 +
   prefix-string + text-record) is still emitted when the value is an
   inline string, but for `xmlns:s="...soap-envelope"` (dict id 4) we
   now emit the short `0b 01 73 04` form WCF uses.

4. **UniqueIdText (0xAC)** added to `NbfxText` enum + encode/decode.
   WCF emits `<a:MessageID>` as a UniqueIdText carrying the 16 raw
   UUID bytes (NOT the `urn:uuid:...` text form). Updated
   `encode_envelope` to use this for MessageID.

Combined wire-byte impact: our envelope body section now matches the
.NET probe byte-for-byte through `<a:Action>`, `<h:ConnectionValidator>`,
`<a:MessageID>` (UniqueId), `<a:ReplyTo>`, `<a:To>`, and `<s:Body>`.
The trailing `01 01 01 01` = 4 EndElements is now the correct
record byte. Tests pass (702 total).

Live status: still TCP RST after the SizedEnvelope. Remaining
unknown is in the body section — the .NET capture shows xmlns:xsi /
xmlns:xsd declarations on the operation-specific request element
(ConnectRequest etc.) that we don't emit, plus possibly different
field encoding inside ConnectRequest. Next iteration will re-capture
through the relay and diff our body bytes against the new
.NET-byte-equivalent we now produce.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-05 15:48:03 -04:00
parent 2867310817
commit c2222b16b0
2 changed files with 134 additions and 11 deletions
+15 -7
View File
@@ -260,13 +260,16 @@ pub fn encode_envelope(
encode_validator(&mut tokens, v, dynamic);
}
// <a:MessageID>urn:uuid:{uuid}</a:MessageID>
let message_id = format!("urn:uuid:{}", make_random_uuid_v4());
// <a:MessageID>{16-byte UUID via UniqueIdText}</a:MessageID>
// WCF emits MessageID as a UniqueIdText record (0xAC) carrying the
// 16 raw UUID bytes — NOT as Chars text. Verified against .NET
// probe wire capture.
let message_id_bytes = make_random_uuid_v4_bytes();
tokens.push(NbfxToken::Element {
prefix: Some("a".to_string()),
name: NbfxName::Static(26),
});
tokens.push(NbfxToken::Text(NbfxText::Chars(message_id)));
tokens.push(NbfxToken::Text(NbfxText::UniqueId(message_id_bytes)));
tokens.push(NbfxToken::EndElement); // </a:MessageID>
// <a:ReplyTo><a:Address>{anonymous}</a:Address></a:ReplyTo>
@@ -587,15 +590,20 @@ fn push_dc_field(out: &mut Vec<NbfxToken>, name: &str, dc_ns: &str, value: &str)
out.push(NbfxToken::EndElement);
}
/// Random RFC 4122 v4-shaped UUID (without pulling the `uuid` crate).
/// Used by `encode_envelope` for the `<a:MessageID>urn:uuid:...`
/// header. The output is a hyphenated lowercase 36-char string.
fn make_random_uuid_v4() -> String {
/// Random RFC 4122 v4 UUID raw bytes. Used by `encode_envelope` for
/// the `<a:MessageID>` UniqueIdText record (16 raw bytes on the wire).
fn make_random_uuid_v4_bytes() -> [u8; 16] {
use rand::RngCore;
let mut bytes = [0u8; 16];
rand::thread_rng().fill_bytes(&mut bytes);
bytes[6] = (bytes[6] & 0x0F) | 0x40; // version 4
bytes[8] = (bytes[8] & 0x3F) | 0x80; // variant 1 (RFC 4122)
bytes
}
#[allow(dead_code)] // kept for callers that need the textual form
fn _unused_make_random_uuid_v4() -> String {
let bytes = make_random_uuid_v4_bytes();
format!(
"{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
bytes[0],