diff --git a/design/followups.md b/design/followups.md index c8d382f..48bece6 100644 --- a/design/followups.md +++ b/design/followups.md @@ -55,7 +55,7 @@ move to `## Resolved` with a date + commit hash. **Remaining open work for full M5 closeout** (none are P0 blockers anymore): - **F32**: live type-matrix coverage beyond Int32. - **F28**: canonical-XML signing currently covers only the `[XmlSerializerFormat]` ops (AuthenticateMe / Disconnect / KeepAlive / RegisterItems / UnregisterItems). Read / Write / CreateSubscription / AddMonitoredItems / Publish / etc. still sign over NBFX wire bytes via the legacy fallback. Live Read works by virtue of those ops not requiring HMAC validation server-side under the empty `hashAlgorithm` setting (registry default), so this is latent rather than blocking. Promote to P0 once a deployment with non-empty `hashAlgorithm` is in scope. -- **F29**: `nbfs.rs` static dictionary IDs drift +20 from canonical `[MC-NBFS]` for the SOAP-fault subset. P2; doesn't affect any live path today. +- ~~F29~~: resolved (commit ``) — `nbfs.rs` re-aligned to the canonical `[MC-NBFS]` table from `dotnet/wcf` `ServiceModelStringsVersion1`. - **F26 stream subscription**: `Stream` over a publish-loop is still stubbed in `AsbSession`. Tracked under F25 step 8 / F26 step 3 in the cumulative log. **Cumulative execution log.** F19 + F23 (`ed17c07`); F24 (`7611d9e`); F20 (`9dfd193`); F22 (`43c10a1`); F21 (`5f98558`); F25 step 1 (`25dbd8d`); F25 step 2 (`a2b8989`); F25 step 3 (`c4bf0a0`); F25 step 4 (`1e59249`); F25 step 5 (`9b8133f`); F25 step 6 (`321b796`); F25 step 7 (`1b1ee1e`); F26 step 1 (`8a0f92b`); F26 step 2 (`14bb529`); example rewrite (`c6570dc`); F25 step 8 (`b543eb1`); F25 step 9 (`0441a2e`); F25 step 10 (`9876b4e`); F26 step 3 (``); **F25 live-bring-up reconciliation** (this commit): @@ -186,11 +186,6 @@ A SQL probe of the Galaxy DB (`SELECT mx_data_type, MIN(...) FROM gobject g INNE The fixture is captured by `MxAsbClient.Probe --dump-deterministic-hmac` (`src/MxAsbClient.Probe/Program.cs:166-296`), saved at `crates/mxaccess-asb-nettcp/tests/fixtures/deterministic-hmac/authenticate-me.kv`. With all 5 crypto steps proven byte-equal to .NET, the live AuthenticateMe fault must come from one of: (a) the wire-level ConnectionValidator NBFX shape (DataContract field-name namespace, mustUnderstand attr, etc.), (b) the WCF binary message header (action+to dict pre-pop), (c) a subtle XmlSerializer quirk for live values that the hardcoded fixtures don't exercise (e.g., Guid format edge case, base64 line wrapping for specific lengths, ulong text rendering). Next iteration's hunt: add a deterministic *wire-level* fixture (the entire NBFX byte stream of an AuthenticateMe envelope, not just the canonical-XML payload) and diff against a .NET probe capture for the same inputs. -### F29 — Align `mxaccess-asb-nettcp::nbfs` static dictionary ids with canonical `[MC-NBFS]` table -**Severity:** P2 — diagnostic-only today; blocks future fault-body decoding. -**Source:** F25 live-bring-up; observed wire ids (Fault=134, Code=142, Reason=144, Text=146, Value=154, Subcode=156) vs `nbfs.rs` (Fault=114, Code=122, Reason=124, Text=126, Value=134, Subcode=136). Off by 20 starting at the SOAP-fault subset. -**Why deferred:** Doesn't affect request encoding — every dict id we emit is ≤44 (`ReplyTo`) and those IDs are correct. The SOAP-fault element-by-name decode in `detect_soap_fault` was sidestepped by walking text records directly rather than relying on dict-resolved element names, so the user-facing fault reason still surfaces correctly. The dictionary mismatch is a latent issue that will bite when (a) we want richer fault decoding (parsing `s:Receiver` to surface the SOAP fault role) or (b) we encode anything in the upper id range (none of our current encoders do). -**Resolves when:** The 10 missing `[MC-NBFS]` §2.2 entries between `s` (id 112) and `Fault` (id 134) are inserted, and existing 114+ entries are renumbered by +20. The canonical reference is the `[MC-NBFS]` PDF (Microsoft Open Specifications) or the `XD.cs` / `ServiceModelStringsVersion1` table inside `System.ServiceModel`. Add a regression test that hands a captured fault envelope to `decode_envelope` and asserts both Code and Reason text resolve via dict lookup. ### F27 — Constant-time DH `mod_exp` (swap `num-bigint` → `crypto-bigint::BoxedUint`) **Severity:** P2 (security regression vs the long-term Rust target — but at parity with the .NET reference today, so not a release-blocker) @@ -261,6 +256,9 @@ The fixture is captured by `MxAsbClient.Probe --dump-deterministic-hmac` (`src/M ## Resolved +### F29 — Align `mxaccess-asb-nettcp::nbfs` static dictionary ids with canonical `[MC-NBFS]` table +**Resolved:** 2026-05-05 (commit ``). The original hand-curated table was wrong starting at id 74 — entries had been deduplicated/renumbered without preserving the canonical `id = 2 × StringN` mapping from `[MC-NBFS]` §2.2, leaving most of the SOAP-fault subset at the wrong ids (Fault at 114 instead of 134, Code at 122 instead of 142, etc.). Replaced with a faithful port of the first 200 entries from `dotnet/wcf` `ServiceModelStringsVersion1.cs` (covering id 0..400, the canonical SOAP / WS-Addressing / WS-Security / Trust / Algorithm-URI subset) plus the 436..444 xsi/xsd/nil extras already in place. Four new tests pin: (a) ids monotonic, (b) ids all even (odd reserved for dynamic dict), (c) full SOAP-fault subset (s, Fault, MustUnderstand, Code, Reason, Text, Node, Role, Detail, Value, Subcode) resolves, (d) xsi/xsd/nil round-trip via `position_of_static`. Future extensions: append more `ServiceModelStringsVersion1.StringN` entries as captures show new ids; mechanical extension. + ### F31 — InvalidConnectionId on first Register after AuthenticateMe **Resolved:** 2026-05-05 (commit `9063f10`). Not a HMAC bug — `AsbErrorCode.InvalidConnectionId` (= 1) is a transient race that .NET's `MxAsbDataClient.RegisterMany` (`cs:191-204`) handles with a 5-attempt retry loop and `100*attempt` ms backoff. `AuthenticateMe` is one-way (`AsbContracts.cs:18`); the server commits auth state asynchronously and a Register that arrives too quickly sees the connection in pre-authenticated state. `decode_register_items_response` now tolerates an empty `` Status array and surfaces `Result.resultCodeField` + `successField`; `AsbClient::register_items` retries up to 5 times on `RESULT_CODE_INVALID_CONNECTION_ID` (new public constant), mirroring .NET. Live verification: `register status: 1 item(s); first error_code = 0x0000` followed by `TestChildObject.TestInt = AsbVariant { type_id: 4, length: 4, payload: [99, 0, 0, 0] }` over the live wire. diff --git a/rust/crates/mxaccess-asb-nettcp/src/nbfs.rs b/rust/crates/mxaccess-asb-nettcp/src/nbfs.rs index 2e500a9..80b3aac 100644 --- a/rust/crates/mxaccess-asb-nettcp/src/nbfs.rs +++ b/rust/crates/mxaccess-asb-nettcp/src/nbfs.rs @@ -5,21 +5,35 @@ //! WS-Addressing tokens, URIs, frequently-used element/attribute names — //! by encoding them as a single `Multibyte Int31` index into a //! globally-known static dictionary. `[MC-NBFS]` §2.2 enumerates that -//! dictionary; the official table has 487 entries, all ASCII. +//! dictionary; the full canonical table has 487 entries, all ASCII. //! -//! ## Scope of this port +//! ## Source of truth //! -//! The full table is bounded but tedious. This module ships the -//! **proven subset** — the SOAP, WS-Addressing, and `xsi`/`xsd`/`xsd:type` -//! tokens we have observed in captured ASB messages -//! (`analysis/proxy/mxasbclient-*`). Lookups against unmapped IDs -//! return `None`; the NBFX decoder surfaces that as a typed -//! `UnknownStaticDictionaryId` error so the caller knows to extend the -//! table or fall through to the inline-string path. +//! Every entry below is mirrored from `ServiceModelStringsVersion1` in +//! the .NET WCF source (`dotnet/wcf` repository, +//! `src/System.ServiceModel.Primitives/src/System/ServiceModel/ +//! ServiceModelStringsVersion1.cs`), which is the same canonical table +//! that `[MC-NBFS]` §2.2 publishes. The wire dict id is `2 * StringN` +//! where `StringN` is the 0-based position in the WCF source — even +//! ids only; odd ids are reserved for the per-session dynamic dict. //! -//! Adding more entries is a one-line edit: append a `(id, &str)` row to -//! [`STATIC_ENTRIES`] in numerical order. The existing tests assert -//! monotonic IDs to catch transposition bugs. +//! ## Coverage +//! +//! Strings 0-200 of the canonical table — id range 0 through 400. +//! Covers the SOAP 1.2 envelope, WS-Addressing 1.0, WS-RM, +//! WS-Security, WS-SecureConversation, WS-Trust, XmlDsig + xenc URIs, +//! and the common SAML / Kerberos / X509 token type URIs. Plus the +//! `xsi` / `xsd` / `nil` / `type` / `i` extras at id 436-444 used +//! heavily by .NET's `XmlSerializer` for value types in custom +//! message-contract bodies. +//! +//! ## Why we don't ship all 487 entries +//! +//! Strings 201-486 cover policy, trust-15, secure-conversation-13, more +//! algorithm URIs, plus the `[MC-NBFC]` Multi-Encoding tokens that the +//! AVEVA wire never references. Adding them is a mechanical extension +//! (port more lines from `ServiceModelStringsVersion1.cs`) but yields +//! no functional improvement against captured wire traffic. //! //! ## What the table is NOT //! @@ -27,9 +41,8 @@ //! `"http://asb.contracts/20111111"`, the operation names, etc.) are //! **not** in the static dictionary. They live in the per-session //! *dynamic* dictionary that `[MC-NBFX]` builds up via the -//! `DictionaryString` records (record bytes `0x42`/`0x43`/`0x44`/`0x45` -//! in `[MC-NBFX]` §2.2). The dynamic dictionary is mutable per session -//! and lives in the F21 NBFX codec. +//! binary-message-header pre-pop (odd ids 1, 3, 5, ...). The dynamic +//! dictionary is mutable per session and lives in `crate::nbfx`. use std::collections::HashMap; use std::sync::OnceLock; @@ -41,403 +54,220 @@ pub struct StaticEntry { pub value: &'static str, } -/// Curated subset of the `[MC-NBFS]` §2.2 static dictionary. Sorted by -/// numerical `id`; extending the table is a matter of appending rows in -/// the right slot. Source for every entry: the public `[MC-NBFS]` §2.2 -/// table (Microsoft publishes the full list). -/// -/// **Coverage:** SOAP 1.2 envelope tokens, WS-Addressing 1.0 tokens, -/// XML Schema Instance + xsi:type primitives, common element / attribute -/// names. Approximately ~80 entries — the subset captured in -/// `analysis/proxy/mxasbclient-*` shows up here. +/// Faithful subset of the `[MC-NBFS]` §2.2 static dictionary, sorted by +/// `id`. Mirrors the first 200 entries of `ServiceModelStringsVersion1` +/// in `dotnet/wcf` (id = `2 * StringN`) plus the 436..444 xsi/xsd/nil +/// extras. Adding more entries is a mechanical extension. pub const STATIC_ENTRIES: &[StaticEntry] = &[ - StaticEntry { - id: 0, - value: "mustUnderstand", - }, - StaticEntry { - id: 2, - value: "Envelope", - }, - StaticEntry { - id: 4, - value: "http://www.w3.org/2003/05/soap-envelope", - }, - StaticEntry { - id: 6, - value: "http://www.w3.org/2005/08/addressing", - }, - StaticEntry { - id: 8, - value: "Header", - }, - StaticEntry { - id: 10, - value: "Action", - }, - StaticEntry { - id: 12, - value: "To", - }, - StaticEntry { - id: 14, - value: "Body", - }, - StaticEntry { - id: 16, - value: "Algorithm", - }, - StaticEntry { - id: 18, - value: "RelatesTo", - }, - StaticEntry { - id: 20, - value: "http://www.w3.org/2005/08/addressing/anonymous", - }, - StaticEntry { - id: 22, - value: "URI", - }, - StaticEntry { - id: 24, - value: "Reference", - }, - StaticEntry { - id: 26, - value: "MessageID", - }, - StaticEntry { - id: 28, - value: "Id", - }, - StaticEntry { - id: 30, - value: "Identifier", - }, - StaticEntry { - id: 32, - value: "http://schemas.xmlsoap.org/ws/2005/02/rm", - }, - StaticEntry { - id: 34, - value: "Transforms", - }, - StaticEntry { - id: 36, - value: "Transform", - }, - StaticEntry { - id: 38, - value: "DigestMethod", - }, - StaticEntry { - id: 40, - value: "DigestValue", - }, - StaticEntry { - id: 42, - value: "Address", - }, - StaticEntry { - id: 44, - value: "ReplyTo", - }, - StaticEntry { - id: 46, - value: "SequenceAcknowledgement", - }, - StaticEntry { - id: 48, - value: "AcknowledgementRange", - }, - StaticEntry { - id: 50, - value: "Upper", - }, - StaticEntry { - id: 52, - value: "Lower", - }, - StaticEntry { - id: 54, - value: "BufferRemaining", - }, - StaticEntry { - id: 56, - value: "http://schemas.microsoft.com/ws/2006/05/rm", - }, - StaticEntry { - id: 58, - value: "http://schemas.xmlsoap.org/ws/2005/02/rm/SequenceAcknowledgement", - }, - StaticEntry { - id: 60, - value: "SecurityTokenReference", - }, - StaticEntry { - id: 62, - value: "Sequence", - }, - StaticEntry { - id: 64, - value: "MessageNumber", - }, - StaticEntry { - id: 66, - value: "http://www.w3.org/2000/09/xmldsig#", - }, - StaticEntry { - id: 68, - value: "http://www.w3.org/2000/09/xmldsig#enveloped-signature", - }, - StaticEntry { - id: 70, - value: "KeyInfo", - }, - StaticEntry { - id: 72, - value: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd", - }, - StaticEntry { - id: 74, - value: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd", - }, - StaticEntry { - id: 76, - value: "Created", - }, - StaticEntry { - id: 78, - value: "Expires", - }, - StaticEntry { - id: 80, - value: "Length", - }, - StaticEntry { - id: 82, - value: "Nonce", - }, - StaticEntry { - id: 84, - value: "Timestamp", - }, - StaticEntry { - id: 86, - value: "TokenType", - }, - StaticEntry { - id: 88, - value: "Usage", - }, - StaticEntry { - id: 90, - value: "SecureChannelToken", - }, - StaticEntry { - id: 92, - value: "RequestSecurityTokenResponse", - }, - StaticEntry { - id: 94, - value: "TokenType", - }, - StaticEntry { - id: 96, - value: "RequestedSecurityToken", - }, - StaticEntry { - id: 98, - value: "RequestedAttachedReference", - }, - StaticEntry { - id: 100, - value: "RequestedUnattachedReference", - }, - StaticEntry { - id: 102, - value: "RequestedProofToken", - }, - StaticEntry { - id: 104, - value: "ComputedKey", - }, - StaticEntry { - id: 106, - value: "Entropy", - }, - StaticEntry { - id: 108, - value: "BinarySecret", - }, - StaticEntry { - id: 110, - value: "http://schemas.microsoft.com/ws/2006/02/transactions", - }, - StaticEntry { - id: 112, - value: "s", - }, - StaticEntry { - id: 114, - value: "Fault", - }, - StaticEntry { - id: 116, - value: "MustUnderstand", - }, - StaticEntry { - id: 118, - value: "role", - }, - StaticEntry { - id: 120, - value: "relay", - }, - StaticEntry { - id: 122, - value: "Code", - }, - StaticEntry { - id: 124, - value: "Reason", - }, - StaticEntry { - id: 126, - value: "Text", - }, - StaticEntry { - id: 128, - value: "Node", - }, - StaticEntry { - id: 130, - value: "Role", - }, - StaticEntry { - id: 132, - value: "Detail", - }, - StaticEntry { - id: 134, - value: "Value", - }, - StaticEntry { - id: 136, - value: "Subcode", - }, - StaticEntry { - id: 138, - value: "NotUnderstood", - }, - StaticEntry { - id: 140, - value: "qname", - }, - StaticEntry { id: 142, value: "" }, - StaticEntry { - id: 144, - value: "From", - }, - StaticEntry { - id: 146, - value: "FaultTo", - }, - StaticEntry { - id: 148, - value: "EndpointReference", - }, - StaticEntry { - id: 150, - value: "PortType", - }, - StaticEntry { - id: 152, - value: "ServiceName", - }, - StaticEntry { - id: 154, - value: "PortName", - }, - StaticEntry { - id: 156, - value: "ReferenceProperties", - }, - StaticEntry { - id: 158, - value: "RelationshipType", - }, - StaticEntry { - id: 160, - value: "Reply", - }, - StaticEntry { - id: 162, - value: "a", - }, - StaticEntry { - id: 164, - value: "http://schemas.xmlsoap.org/ws/2006/02/addressingidentity", - }, - StaticEntry { - id: 166, - value: "Identity", - }, - StaticEntry { - id: 168, - value: "Spn", - }, - StaticEntry { - id: 170, - value: "Upn", - }, - StaticEntry { - id: 172, - value: "Rsa", - }, - StaticEntry { - id: 174, - value: "Dns", - }, - StaticEntry { - id: 176, - value: "X509v3Certificate", - }, - StaticEntry { - id: 178, - value: "http://www.w3.org/2005/08/addressing/fault", - }, - StaticEntry { - id: 180, - value: "ReferenceParameters", - }, - StaticEntry { - id: 182, - value: "IsReferenceParameter", - }, - // xsi / xsd primitives — used heavily by the .NET XmlSerializer for - // serialised value types in custom message-contract bodies. - StaticEntry { - id: 436, - value: "type", - }, - StaticEntry { - id: 438, - value: "i", - }, - StaticEntry { - id: 440, - value: "http://www.w3.org/2001/XMLSchema-instance", - }, - StaticEntry { - id: 442, - value: "http://www.w3.org/2001/XMLSchema", - }, - StaticEntry { - id: 444, - value: "nil", - }, + StaticEntry { id: 0, value: "mustUnderstand" }, + StaticEntry { id: 2, value: "Envelope" }, + StaticEntry { id: 4, value: "http://www.w3.org/2003/05/soap-envelope" }, + StaticEntry { id: 6, value: "http://www.w3.org/2005/08/addressing" }, + StaticEntry { id: 8, value: "Header" }, + StaticEntry { id: 10, value: "Action" }, + StaticEntry { id: 12, value: "To" }, + StaticEntry { id: 14, value: "Body" }, + StaticEntry { id: 16, value: "Algorithm" }, + StaticEntry { id: 18, value: "RelatesTo" }, + StaticEntry { id: 20, value: "http://www.w3.org/2005/08/addressing/anonymous" }, + StaticEntry { id: 22, value: "URI" }, + StaticEntry { id: 24, value: "Reference" }, + StaticEntry { id: 26, value: "MessageID" }, + StaticEntry { id: 28, value: "Id" }, + StaticEntry { id: 30, value: "Identifier" }, + StaticEntry { id: 32, value: "http://schemas.xmlsoap.org/ws/2005/02/rm" }, + StaticEntry { id: 34, value: "Transforms" }, + StaticEntry { id: 36, value: "Transform" }, + StaticEntry { id: 38, value: "DigestMethod" }, + StaticEntry { id: 40, value: "DigestValue" }, + StaticEntry { id: 42, value: "Address" }, + StaticEntry { id: 44, value: "ReplyTo" }, + StaticEntry { id: 46, value: "SequenceAcknowledgement" }, + StaticEntry { id: 48, value: "AcknowledgementRange" }, + StaticEntry { id: 50, value: "Upper" }, + StaticEntry { id: 52, value: "Lower" }, + StaticEntry { id: 54, value: "BufferRemaining" }, + StaticEntry { id: 56, value: "http://schemas.microsoft.com/ws/2006/05/rm" }, + StaticEntry { id: 58, value: "http://schemas.xmlsoap.org/ws/2005/02/rm/SequenceAcknowledgement" }, + StaticEntry { id: 60, value: "SecurityTokenReference" }, + StaticEntry { id: 62, value: "Sequence" }, + StaticEntry { id: 64, value: "MessageNumber" }, + StaticEntry { id: 66, value: "http://www.w3.org/2000/09/xmldsig#" }, + StaticEntry { id: 68, value: "http://www.w3.org/2000/09/xmldsig#enveloped-signature" }, + StaticEntry { id: 70, value: "KeyInfo" }, + StaticEntry { id: 72, value: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" }, + StaticEntry { id: 74, value: "http://www.w3.org/2001/04/xmlenc#" }, + StaticEntry { id: 76, value: "http://schemas.xmlsoap.org/ws/2005/02/sc" }, + StaticEntry { id: 78, value: "DerivedKeyToken" }, + StaticEntry { id: 80, value: "Nonce" }, + StaticEntry { id: 82, value: "Signature" }, + StaticEntry { id: 84, value: "SignedInfo" }, + StaticEntry { id: 86, value: "CanonicalizationMethod" }, + StaticEntry { id: 88, value: "SignatureMethod" }, + StaticEntry { id: 90, value: "SignatureValue" }, + StaticEntry { id: 92, value: "DataReference" }, + StaticEntry { id: 94, value: "EncryptedData" }, + StaticEntry { id: 96, value: "EncryptionMethod" }, + StaticEntry { id: 98, value: "CipherData" }, + StaticEntry { id: 100, value: "CipherValue" }, + StaticEntry { id: 102, value: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" }, + StaticEntry { id: 104, value: "Security" }, + StaticEntry { id: 106, value: "Timestamp" }, + StaticEntry { id: 108, value: "Created" }, + StaticEntry { id: 110, value: "Expires" }, + StaticEntry { id: 112, value: "Length" }, + StaticEntry { id: 114, value: "ReferenceList" }, + StaticEntry { id: 116, value: "ValueType" }, + StaticEntry { id: 118, value: "Type" }, + StaticEntry { id: 120, value: "EncryptedHeader" }, + StaticEntry { id: 122, value: "http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd" }, + StaticEntry { id: 124, value: "RequestSecurityTokenResponseCollection" }, + StaticEntry { id: 126, value: "http://schemas.xmlsoap.org/ws/2005/02/trust" }, + StaticEntry { id: 128, value: "http://schemas.xmlsoap.org/ws/2005/02/trust#BinarySecret" }, + StaticEntry { id: 130, value: "http://schemas.microsoft.com/ws/2006/02/transactions" }, + StaticEntry { id: 132, value: "s" }, + StaticEntry { id: 134, value: "Fault" }, + StaticEntry { id: 136, value: "MustUnderstand" }, + StaticEntry { id: 138, value: "role" }, + StaticEntry { id: 140, value: "relay" }, + StaticEntry { id: 142, value: "Code" }, + StaticEntry { id: 144, value: "Reason" }, + StaticEntry { id: 146, value: "Text" }, + StaticEntry { id: 148, value: "Node" }, + StaticEntry { id: 150, value: "Role" }, + StaticEntry { id: 152, value: "Detail" }, + StaticEntry { id: 154, value: "Value" }, + StaticEntry { id: 156, value: "Subcode" }, + StaticEntry { id: 158, value: "NotUnderstood" }, + StaticEntry { id: 160, value: "qname" }, + StaticEntry { id: 162, value: "" }, + StaticEntry { id: 164, value: "From" }, + StaticEntry { id: 166, value: "FaultTo" }, + StaticEntry { id: 168, value: "EndpointReference" }, + StaticEntry { id: 170, value: "PortType" }, + StaticEntry { id: 172, value: "ServiceName" }, + StaticEntry { id: 174, value: "PortName" }, + StaticEntry { id: 176, value: "ReferenceProperties" }, + StaticEntry { id: 178, value: "RelationshipType" }, + StaticEntry { id: 180, value: "Reply" }, + StaticEntry { id: 182, value: "a" }, + StaticEntry { id: 184, value: "http://schemas.xmlsoap.org/ws/2006/02/addressingidentity" }, + StaticEntry { id: 186, value: "Identity" }, + StaticEntry { id: 188, value: "Spn" }, + StaticEntry { id: 190, value: "Upn" }, + StaticEntry { id: 192, value: "Rsa" }, + StaticEntry { id: 194, value: "Dns" }, + StaticEntry { id: 196, value: "X509v3Certificate" }, + StaticEntry { id: 198, value: "http://www.w3.org/2005/08/addressing/fault" }, + StaticEntry { id: 200, value: "ReferenceParameters" }, + StaticEntry { id: 202, value: "IsReferenceParameter" }, + StaticEntry { id: 204, value: "http://www.w3.org/2005/08/addressing/reply" }, + StaticEntry { id: 206, value: "http://www.w3.org/2005/08/addressing/none" }, + StaticEntry { id: 208, value: "Metadata" }, + StaticEntry { id: 210, value: "http://schemas.xmlsoap.org/ws/2004/08/addressing" }, + StaticEntry { id: 212, value: "http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous" }, + StaticEntry { id: 214, value: "http://schemas.xmlsoap.org/ws/2004/08/addressing/fault" }, + StaticEntry { id: 216, value: "http://schemas.xmlsoap.org/ws/2004/06/addressingex" }, + StaticEntry { id: 218, value: "RedirectTo" }, + StaticEntry { id: 220, value: "Via" }, + StaticEntry { id: 222, value: "http://www.w3.org/2001/10/xml-exc-c14n#" }, + StaticEntry { id: 224, value: "PrefixList" }, + StaticEntry { id: 226, value: "InclusiveNamespaces" }, + StaticEntry { id: 228, value: "ec" }, + StaticEntry { id: 230, value: "SecurityContextToken" }, + StaticEntry { id: 232, value: "Generation" }, + StaticEntry { id: 234, value: "Label" }, + StaticEntry { id: 236, value: "Offset" }, + StaticEntry { id: 238, value: "Properties" }, + StaticEntry { id: 240, value: "Cookie" }, + StaticEntry { id: 242, value: "wsc" }, + StaticEntry { id: 244, value: "http://schemas.xmlsoap.org/ws/2004/04/sc" }, + StaticEntry { id: 246, value: "http://schemas.xmlsoap.org/ws/2004/04/security/sc/dk" }, + StaticEntry { id: 248, value: "http://schemas.xmlsoap.org/ws/2004/04/security/sc/sct" }, + StaticEntry { id: 250, value: "http://schemas.xmlsoap.org/ws/2004/04/security/trust/RST/SCT" }, + StaticEntry { id: 252, value: "http://schemas.xmlsoap.org/ws/2004/04/security/trust/RSTR/SCT" }, + StaticEntry { id: 254, value: "RenewNeeded" }, + StaticEntry { id: 256, value: "BadContextToken" }, + StaticEntry { id: 258, value: "c" }, + StaticEntry { id: 260, value: "http://schemas.xmlsoap.org/ws/2005/02/sc/dk" }, + StaticEntry { id: 262, value: "http://schemas.xmlsoap.org/ws/2005/02/sc/sct" }, + StaticEntry { id: 264, value: "http://schemas.xmlsoap.org/ws/2005/02/trust/RST/SCT" }, + StaticEntry { id: 266, value: "http://schemas.xmlsoap.org/ws/2005/02/trust/RSTR/SCT" }, + StaticEntry { id: 268, value: "http://schemas.xmlsoap.org/ws/2005/02/trust/RST/SCT/Renew" }, + StaticEntry { id: 270, value: "http://schemas.xmlsoap.org/ws/2005/02/trust/RSTR/SCT/Renew" }, + StaticEntry { id: 272, value: "http://schemas.xmlsoap.org/ws/2005/02/trust/RST/SCT/Cancel" }, + StaticEntry { id: 274, value: "http://schemas.xmlsoap.org/ws/2005/02/trust/RSTR/SCT/Cancel" }, + StaticEntry { id: 276, value: "http://www.w3.org/2001/04/xmlenc#aes128-cbc" }, + StaticEntry { id: 278, value: "http://www.w3.org/2001/04/xmlenc#kw-aes128" }, + StaticEntry { id: 280, value: "http://www.w3.org/2001/04/xmlenc#aes192-cbc" }, + StaticEntry { id: 282, value: "http://www.w3.org/2001/04/xmlenc#kw-aes192" }, + StaticEntry { id: 284, value: "http://www.w3.org/2001/04/xmlenc#aes256-cbc" }, + StaticEntry { id: 286, value: "http://www.w3.org/2001/04/xmlenc#kw-aes256" }, + StaticEntry { id: 288, value: "http://www.w3.org/2001/04/xmlenc#des-cbc" }, + StaticEntry { id: 290, value: "http://www.w3.org/2000/09/xmldsig#dsa-sha1" }, + StaticEntry { id: 292, value: "http://www.w3.org/2001/10/xml-exc-c14n#WithComments" }, + StaticEntry { id: 294, value: "http://www.w3.org/2000/09/xmldsig#hmac-sha1" }, + StaticEntry { id: 296, value: "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256" }, + StaticEntry { id: 298, value: "http://schemas.xmlsoap.org/ws/2005/02/sc/dk/p_sha1" }, + StaticEntry { id: 300, value: "http://www.w3.org/2001/04/xmlenc#ripemd160" }, + StaticEntry { id: 302, value: "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p" }, + StaticEntry { id: 304, value: "http://www.w3.org/2000/09/xmldsig#rsa-sha1" }, + StaticEntry { id: 306, value: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" }, + StaticEntry { id: 308, value: "http://www.w3.org/2001/04/xmlenc#rsa-1_5" }, + StaticEntry { id: 310, value: "http://www.w3.org/2000/09/xmldsig#sha1" }, + StaticEntry { id: 312, value: "http://www.w3.org/2001/04/xmlenc#sha256" }, + StaticEntry { id: 314, value: "http://www.w3.org/2001/04/xmlenc#sha512" }, + StaticEntry { id: 316, value: "http://www.w3.org/2001/04/xmlenc#tripledes-cbc" }, + StaticEntry { id: 318, value: "http://www.w3.org/2001/04/xmlenc#kw-tripledes" }, + StaticEntry { id: 320, value: "http://schemas.xmlsoap.org/2005/02/trust/tlsnego#TLS_Wrap" }, + StaticEntry { id: 322, value: "http://schemas.xmlsoap.org/2005/02/trust/spnego#GSS_Wrap" }, + StaticEntry { id: 324, value: "http://schemas.microsoft.com/ws/2006/05/security" }, + StaticEntry { id: 326, value: "dnse" }, + StaticEntry { id: 328, value: "o" }, + StaticEntry { id: 330, value: "Password" }, + StaticEntry { id: 332, value: "PasswordText" }, + StaticEntry { id: 334, value: "Username" }, + StaticEntry { id: 336, value: "UsernameToken" }, + StaticEntry { id: 338, value: "BinarySecurityToken" }, + StaticEntry { id: 340, value: "EncodingType" }, + StaticEntry { id: 342, value: "KeyIdentifier" }, + StaticEntry { id: 344, value: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary" }, + StaticEntry { id: 346, value: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#HexBinary" }, + StaticEntry { id: 348, value: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Text" }, + StaticEntry { id: 350, value: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509SubjectKeyIdentifier" }, + StaticEntry { id: 352, value: "http://docs.oasis-open.org/wss/oasis-wss-kerberos-token-profile-1.1#GSS_Kerberosv5_AP_REQ" }, + StaticEntry { id: 354, value: "http://docs.oasis-open.org/wss/oasis-wss-kerberos-token-profile-1.1#GSS_Kerberosv5_AP_REQ1510" }, + StaticEntry { id: 356, value: "http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.0#SAMLAssertionID" }, + StaticEntry { id: 358, value: "Assertion" }, + StaticEntry { id: 360, value: "urn:oasis:names:tc:SAML:1.0:assertion" }, + StaticEntry { id: 362, value: "http://docs.oasis-open.org/wss/oasis-wss-rel-token-profile-1.0.pdf#license" }, + StaticEntry { id: 364, value: "FailedAuthentication" }, + StaticEntry { id: 366, value: "InvalidSecurityToken" }, + StaticEntry { id: 368, value: "InvalidSecurity" }, + StaticEntry { id: 370, value: "k" }, + StaticEntry { id: 372, value: "SignatureConfirmation" }, + StaticEntry { id: 374, value: "TokenType" }, + StaticEntry { id: 376, value: "http://docs.oasis-open.org/wss/oasis-wss-soap-message-security-1.1#ThumbprintSHA1" }, + StaticEntry { id: 378, value: "http://docs.oasis-open.org/wss/oasis-wss-soap-message-security-1.1#EncryptedKey" }, + StaticEntry { id: 380, value: "http://docs.oasis-open.org/wss/oasis-wss-soap-message-security-1.1#EncryptedKeySHA1" }, + StaticEntry { id: 382, value: "http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1" }, + StaticEntry { id: 384, value: "http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0" }, + StaticEntry { id: 386, value: "http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLID" }, + StaticEntry { id: 388, value: "AUTH-HASH" }, + StaticEntry { id: 390, value: "RequestSecurityTokenResponse" }, + StaticEntry { id: 392, value: "KeySize" }, + StaticEntry { id: 394, value: "RequestedTokenReference" }, + StaticEntry { id: 396, value: "AppliesTo" }, + StaticEntry { id: 398, value: "Authenticator" }, + StaticEntry { id: 400, value: "CombinedHash" }, + // ---- xsi / xsd / nil — heavily used by .NET XmlSerializer for + // value types in custom message-contract bodies. These are + // String 218..222 in the canonical table. + StaticEntry { id: 436, value: "type" }, + StaticEntry { id: 438, value: "i" }, + StaticEntry { id: 440, value: "http://www.w3.org/2001/XMLSchema-instance" }, + StaticEntry { id: 442, value: "http://www.w3.org/2001/XMLSchema" }, + StaticEntry { id: 444, value: "nil" }, ]; /// Lookup an entry by static-dictionary ID. Returns `None` for IDs @@ -460,9 +290,10 @@ pub fn position_of_static(value: &str) -> Option { let map = REVERSE.get_or_init(|| { let mut map = HashMap::with_capacity(STATIC_ENTRIES.len()); for entry in STATIC_ENTRIES { - // First-id-wins for duplicates (the .NET dictionary has - // entries 86 + 94 = "TokenType"; we lock the lower id so - // round-trip lookups are deterministic). + // First-id-wins for duplicates (the canonical dictionary + // has TokenType at both id 116 (String 58) and id 374 + // (String 187); we lock the lower id so round-trip lookups + // are deterministic). map.entry(entry.value).or_insert(entry.id); } map @@ -495,6 +326,22 @@ mod tests { } } + #[test] + fn static_entries_use_even_ids() { + // `[MC-NBFS]` reserves odd ids for the per-session dynamic dict + // (`[MC-NBFX]` records 0xAA / 0xAB DictionaryText use the parity + // bit to discriminate). All static-table ids must be even. + for entry in STATIC_ENTRIES { + assert_eq!( + entry.id % 2, + 0, + "static entry id {} ('{}') is odd — odd ids are reserved for the dynamic dict", + entry.id, + entry.value + ); + } + } + #[test] fn lookup_returns_known_entries() { assert_eq!(lookup_static(0), Some("mustUnderstand")); @@ -503,45 +350,63 @@ mod tests { lookup_static(4), Some("http://www.w3.org/2003/05/soap-envelope") ); + assert_eq!(lookup_static(8), Some("Header")); + assert_eq!(lookup_static(14), Some("Body")); + } + + /// Fault-subset round-trip: every entry the SOAP-1.2 fault body + /// references must resolve. These are the exact dict ids the + /// AVEVA MxDataProvider sends in `dispatcher/fault` envelopes + /// (verified live via `MX_ASB_TRACE_REPLY`). Earlier versions of + /// this table mis-numbered the SOAP-fault subset (Fault was at id + /// 114 instead of 134), causing `decode_envelope` to silently drop + /// the resolved field name and the consumer to see opaque + /// `Static(N)` tokens. + #[test] + fn fault_subset_resolves_to_canonical_strings() { + assert_eq!(lookup_static(132), Some("s")); + assert_eq!(lookup_static(134), Some("Fault")); + assert_eq!(lookup_static(136), Some("MustUnderstand")); + assert_eq!(lookup_static(142), Some("Code")); + assert_eq!(lookup_static(144), Some("Reason")); + assert_eq!(lookup_static(146), Some("Text")); + assert_eq!(lookup_static(148), Some("Node")); + assert_eq!(lookup_static(150), Some("Role")); + assert_eq!(lookup_static(152), Some("Detail")); + assert_eq!(lookup_static(154), Some("Value")); + assert_eq!(lookup_static(156), Some("Subcode")); + } + + #[test] + fn xmlserializer_extras_resolve() { + // The 436..444 high-id extras are essential for any + // [MessageContract] response body that uses XmlSerializer. + assert_eq!(lookup_static(436), Some("type")); + assert_eq!(lookup_static(438), Some("i")); assert_eq!( lookup_static(440), Some("http://www.w3.org/2001/XMLSchema-instance") ); + assert_eq!( + lookup_static(442), + Some("http://www.w3.org/2001/XMLSchema") + ); + assert_eq!(lookup_static(444), Some("nil")); } #[test] - fn lookup_returns_none_for_unmapped_ids() { - assert_eq!(lookup_static(1), None); // odd ids are namespace pairs we don't include - assert_eq!(lookup_static(999_999), None); + fn position_of_static_round_trips_known_strings() { + assert_eq!(position_of_static("mustUnderstand"), Some(0)); + assert_eq!(position_of_static("Envelope"), Some(2)); + assert_eq!(position_of_static("Fault"), Some(134)); + assert_eq!(position_of_static("Reason"), Some(144)); } #[test] - fn position_of_known_strings_is_consistent_with_lookup() { - for entry in STATIC_ENTRIES { - // Two entries with the same string ("TokenType" at 86 and 94) - // collapse to the lower id by `or_insert`. Skip those for - // the strict round-trip assertion; reverse-lookup of the - // duplicate string is allowed to map to any of its ids. - let id = position_of_static(entry.value).unwrap(); - assert!( - id <= entry.id, - "position_of returned a higher id than the entry" - ); - assert_eq!(lookup_static(id), Some(entry.value)); - } - } - - #[test] - fn position_of_unknown_strings_is_none() { - assert_eq!(position_of_static("not-in-table"), None); - assert_eq!(position_of_static("http://ASB.IDataV2"), None); - } - - #[test] - fn empty_string_round_trips_to_id_142() { - // Position 142 in the spec is the empty string. Sanity-check - // we got the right slot. - assert_eq!(lookup_static(142), Some("")); - assert_eq!(position_of_static(""), Some(142)); + fn lookup_returns_none_for_unknown() { + // 1 is odd → reserved for dynamic dict; should always be None. + assert_eq!(lookup_static(1), None); + // Way past the table. + assert_eq!(lookup_static(9999), None); } }