//! `[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 official table has 487 entries, all ASCII. //! //! ## Scope of this port //! //! 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. //! //! 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. //! //! ## 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 //! `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. 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, } /// 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. 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", }, ]; /// 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 .NET dictionary has // entries 86 + 94 = "TokenType"; 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 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(440), Some("http://www.w3.org/2001/XMLSchema-instance") ); } #[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); } #[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)); } }