//! `Util::fetchClientConfig` tables. //! //! Between 227 and 243 key/value rows per CFID, overwhelmingly the same RS4 //! base URL repeated across 212 endpoint keys. The client resolves a per-call //! key (`FUT_RS4_URL_`) before a per-module one //! (`FUT_RS4_APIURL_`), and any call left unresolved falls back to a //! real (dead) EA host — which is what produced "there has been an error //! connecting to FIFA 17 Ultimate Team" mid-session when only the boot subset //! was served. The table has to be complete, not representative. //! //! # Why this is data and not code //! //! The rows are reverse-engineered *configuration*, not logic. They live in //! `fixtures/client_config.json`, derived mechanically from the Python oracle //! and templated on `{advertise}`, `{bind}`, `{pow_content_host}` and //! `{pow_host}` so the adapter stays deployable anywhere. Hand-transcribing 400 //! string literals would add a class of silent typo no reviewer can catch, and //! `openfut-core` already loads its content from `data/` for the same reason. //! //! The generator does not take its own templating on trust: it substitutes real //! addresses back in and diffs against the oracle for every section before //! writing the file. use std::collections::BTreeMap; use std::sync::OnceLock; use super::config::AdapterConfig; /// Rows for every known CFID, plus `__default__` for unknown ones. const TABLE_JSON: &str = include_str!("../../fixtures/client_config.json"); type Table = BTreeMap>; fn table() -> &'static Table { static TABLE: OnceLock = OnceLock::new(); TABLE.get_or_init(|| { serde_json::from_str(TABLE_JSON).expect("bundled client_config.json is valid") }) } /// Resolve the rows for a CFID, with addresses substituted in. /// /// Unknown CFIDs deliberately still receive the shared FUT/RS4/POW rows: those /// consumers read a merged `_all` store and which section contributes is /// unproven, so a present-but-shared table is safer than an empty one. pub fn rows_for(cfid: &str, cfg: &AdapterConfig) -> Vec<(String, String)> { let t = table(); let rows = t .get(cfid) .or_else(|| t.get("__default__")) .expect("client_config.json always carries a __default__ section"); // URL-level tokens resolve through AdapterConfig so those helpers are the // single place a URL shape is defined. Host-level tokens cover the values // that are not one of the three standard URLs (roster, POW API). let utas_base = cfg.utas_base(); let nucleus_base = cfg.nucleus_base(); let pow_content_url = cfg.pow_content_url(); rows.iter() .map(|(k, v)| { let v = if v.contains('{') { v.replace("{utas_base}", &utas_base) .replace("{nucleus_base}", &nucleus_base) .replace("{pow_content_url}", &pow_content_url) .replace("{advertise}", &cfg.endpoints.advertise) .replace("{bind}", &cfg.endpoints.bind) .replace("{pow_content_host}", &cfg.endpoints.pow_content_host) .replace("{pow_host}", &cfg.endpoints.pow_host) } else { v.clone() }; debug_assert!(!v.contains('{'), "unsubstituted token left in {k}: {v}"); (k.clone(), v) }) .collect() } /// Fingerprint of the bundled config table. /// /// The table is generated data, so "which binary is this?" is only half the /// question — "which data does it carry?" is the other half. A running host /// logs this at startup so a live FIFA trace can be tied to an exact table, and /// a rebuild that silently picked up regenerated fixtures is visible. /// /// FNV-1a, not a security hash and never used as one. pub fn table_fingerprint() -> u64 { let mut hash: u64 = 0xcbf2_9ce4_8422_2325; for byte in TABLE_JSON.as_bytes() { hash ^= *byte as u64; hash = hash.wrapping_mul(0x1000_0000_01b3); } hash } /// Every CFID with its own section. Unknown CFIDs are still valid requests. pub fn known_sections() -> Vec<&'static str> { table() .keys() .filter(|k| k.as_str() != "__default__") .map(String::as_str) .collect() } #[cfg(test)] mod tests { use super::*; fn cfg() -> AdapterConfig { let mut c = AdapterConfig::loopback(); c.endpoints.advertise = "198.51.100.7".into(); c.endpoints.bind = "0.0.0.0".into(); c.endpoints.pow_content_host = "198.51.100.7:8085".into(); c.endpoints.pow_host = "198.51.100.7:8094".into(); c } #[test] fn bundled_table_parses() { assert!(table().contains_key("__default__")); assert!(table().contains_key("BlazeSDK")); assert!(known_sections().len() >= 10); } #[test] fn default_section_is_the_shared_fut_base() { let rows = rows_for("literally-anything", &cfg()); assert_eq!(rows.len(), 227); assert!(rows.iter().any(|(k, _)| k == "FUT_RS4_BASE_URL")); } #[test] fn addresses_are_substituted_not_baked() { let rows = rows_for("BlazeSDK", &cfg()); let base = rows .iter() .find(|(k, _)| k == "FUT_RS4_BASE_URL") .expect("base url present"); assert_eq!(base.1, "http://198.51.100.7:8099/"); assert!( !rows.iter().any(|(_, v)| v.contains('{')), "a template token survived substitution" ); } #[test] fn nucleus_follows_bind_reproducing_the_oracle() { let rows = rows_for("BlazeSDK", &cfg()); let n = rows.iter().find(|(k, _)| k == "nucleusConnect").unwrap(); assert_eq!(n.1, "http://0.0.0.0:42131"); } #[test] fn roster_section_carries_the_roster_urls() { let rows = rows_for("OSDK_ROSTER", &cfg()); let r = rows.iter().find(|(k, _)| k == "ROSTER_URL").unwrap(); assert_eq!(r.1, "https://198.51.100.7:8081/fifa17/roster/"); } #[test] fn rows_are_sorted_as_the_wire_requires() { // The oracle sorts; the TDF map encoder does not, so order is ours to keep. let rows = rows_for("BlazeSDK", &cfg()); let mut sorted = rows.clone(); sorted.sort(); assert_eq!(rows, sorted); } #[test] fn every_known_section_substitutes_cleanly() { for cfid in known_sections() { for (k, v) in rows_for(cfid, &cfg()) { assert!(!v.contains('{'), "{cfid}/{k} kept a token: {v}"); } } } }