//! `[MC-NBFS]` static dictionary table for `[MC-NBFX]` binary XML. //! //! The .NET binary message encoder (`BinaryMessageEncodingBindingElement`, //! the default for `NetTcpBinding`) compresses common strings — SOAP / //! 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 full canonical table has 487 entries, all ASCII. //! //! ## Source of truth //! //! 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. //! //! ## 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 //! //! ASB-specific contract strings (`"http://ASB.IDataV2"`, //! `"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 //! 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; /// One static-dictionary entry. #[derive(Debug, Clone, Copy)] pub struct StaticEntry { pub id: u32, pub value: &'static str, } /// 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://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 /// outside the curated subset; callers should treat that as "unknown /// static ID" and either extend [`STATIC_ENTRIES`] or fall through to /// the inline-string path. pub fn lookup_static(id: u32) -> Option<&'static str> { STATIC_ENTRIES .binary_search_by_key(&id, |e| e.id) .ok() .and_then(|idx| STATIC_ENTRIES.get(idx).map(|e| e.value)) } /// Reverse lookup — find the static-dictionary ID for a string. Returns /// `None` for strings not in the curated subset; encoders can either /// extend [`STATIC_ENTRIES`] or fall through to the inline-string / /// dynamic-dictionary path. pub fn position_of_static(value: &str) -> Option { static REVERSE: OnceLock> = OnceLock::new(); 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 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 }); map.get(value).copied() } #[cfg(test)] #[allow( clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::indexing_slicing )] mod tests { use super::*; #[test] fn static_entries_have_monotonic_ids() { let mut last = None; for entry in STATIC_ENTRIES { if let Some(prev) = last { assert!( entry.id > prev, "static dictionary entries must be sorted by id; saw {prev} then {}", entry.id ); } last = Some(entry.id); } } #[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")); assert_eq!(lookup_static(2), Some("Envelope")); assert_eq!( 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 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 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); } }