1use std::collections::HashMap;
8use std::path::Path;
9
10use crate::error::SaveError;
11
12#[derive(Debug, serde::Deserialize)]
14struct MappingEntry {
15 #[serde(rename = "Key")]
16 key: String,
17 #[serde(rename = "Value")]
18 value: String,
19}
20
21#[derive(Debug, serde::Deserialize)]
23struct MappingFile {
24 #[serde(rename = "libMBIN_version")]
25 version: String,
26 #[serde(rename = "Mapping")]
27 mapping: Vec<MappingEntry>,
28}
29
30#[derive(Debug, Clone)]
35pub struct KeyMapping {
36 pub version: String,
38 entries: HashMap<String, String>,
40 fixups: HashMap<String, String>,
42}
43
44impl KeyMapping {
45 pub fn bundled() -> Self {
52 let primary_json = include_str!("../data/mapping_mbincompiler.json");
53 let legacy_json = include_str!("../data/mapping_legacy.json");
54 let savewizard_json = include_str!("../data/mapping_savewizard.json");
55
56 let mut mapping = Self::from_json(primary_json)
57 .expect("bundled mapping_mbincompiler.json should be valid");
58
59 let legacy: MappingFile =
60 serde_json::from_str(legacy_json).expect("bundled mapping_legacy.json should be valid");
61 for entry in legacy.mapping {
62 mapping.entries.entry(entry.key).or_insert(entry.value);
63 }
64
65 let savewizard: MappingFile = serde_json::from_str(savewizard_json)
66 .expect("bundled mapping_savewizard.json should be valid");
67 for entry in savewizard.mapping {
68 mapping.fixups.insert(entry.key, entry.value);
69 }
70
71 mapping
72 }
73
74 pub fn from_json(json: &str) -> Result<Self, SaveError> {
76 let file: MappingFile =
77 serde_json::from_str(json).map_err(|e| SaveError::MappingParseError {
78 message: e.to_string(),
79 })?;
80
81 let mut entries = HashMap::with_capacity(file.mapping.len());
82 for entry in file.mapping {
83 entries.entry(entry.key).or_insert(entry.value);
84 }
85
86 Ok(Self {
87 version: file.version,
88 entries,
89 fixups: HashMap::new(),
90 })
91 }
92
93 pub fn from_file(path: &Path) -> Result<Self, SaveError> {
95 let json = std::fs::read_to_string(path)?;
96 Self::from_json(&json)
97 }
98
99 pub fn get(&self, obfuscated: &str) -> Option<&str> {
101 self.entries.get(obfuscated).map(|s| s.as_str())
102 }
103
104 pub fn len(&self) -> usize {
106 self.entries.len()
107 }
108
109 pub fn is_empty(&self) -> bool {
111 self.entries.is_empty()
112 }
113
114 pub fn deobfuscate(&self, value: &mut serde_json::Value) {
120 self.walk(value);
121 }
122
123 fn walk(&self, value: &mut serde_json::Value) {
124 match value {
125 serde_json::Value::Object(map) => {
126 let entries: Vec<(String, serde_json::Value)> = std::mem::take(map)
127 .into_iter()
128 .map(|(k, mut v)| {
129 self.walk(&mut v);
130 let mut new_key = self.entries.get(&k).cloned().unwrap_or(k);
131 if let Some(fixed) = self.fixups.get(&new_key) {
132 new_key = fixed.clone();
133 }
134 (new_key, v)
135 })
136 .collect();
137 *map = serde_json::Map::from_iter(entries);
138 }
139 serde_json::Value::Array(arr) => {
140 for item in arr.iter_mut() {
141 self.walk(item);
142 }
143 }
144 _ => {}
145 }
146 }
147}
148
149pub fn is_obfuscated(json: &serde_json::Value) -> bool {
154 match json.as_object() {
155 Some(map) => {
156 if map.contains_key("F2P") {
157 return true;
158 }
159 if map.contains_key("Version") {
160 return false;
161 }
162 map.contains_key("6f=") || map.contains_key("8>q")
163 }
164 None => false,
165 }
166}
167
168pub fn deobfuscate_json(
172 json_bytes: &[u8],
173 mapping: &KeyMapping,
174) -> Result<serde_json::Value, SaveError> {
175 let mut value: serde_json::Value =
176 serde_json::from_slice(json_bytes).map_err(|e| SaveError::JsonParseError {
177 message: e.to_string(),
178 })?;
179
180 if is_obfuscated(&value) {
181 mapping.deobfuscate(&mut value);
182 }
183
184 Ok(value)
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190 use serde_json::json;
191
192 #[test]
193 fn load_bundled_mapping() {
194 let mapping = KeyMapping::bundled();
195 assert!(
196 mapping.len() > 1300,
197 "expected 1300+ entries, got {}",
198 mapping.len()
199 );
200 assert_eq!(mapping.version, "6.11.0.1");
201 }
202
203 #[test]
204 fn lookup_known_keys() {
205 let mapping = KeyMapping::bundled();
206 assert_eq!(mapping.get("F2P"), Some("Version"));
207 assert_eq!(mapping.get("6f="), Some("PlayerStateData"));
208 assert_eq!(mapping.get("8>q"), Some("Platform"));
209 }
210
211 #[test]
212 fn lookup_unknown_key() {
213 let mapping = KeyMapping::bundled();
214 assert_eq!(mapping.get("ZZZNOTAKEY"), None);
215 }
216
217 #[test]
218 fn legacy_keys_merged() {
219 let mapping = KeyMapping::bundled();
220 assert_eq!(mapping.get("5Ta"), Some("AllowFriendBases"));
221 }
222
223 #[test]
224 fn deobfuscate_simple() {
225 let mapping = KeyMapping::bundled();
226 let mut value = json!({"F2P": 6726});
227 mapping.deobfuscate(&mut value);
228 assert_eq!(value, json!({"Version": 6726}));
229 }
230
231 #[test]
232 fn deobfuscate_nested() {
233 let mapping = KeyMapping::bundled();
234 assert_eq!(mapping.get("Pk4"), Some("SaveName"));
235 let mut value = json!({"6f=": {"Pk4": "MySave"}});
236 mapping.deobfuscate(&mut value);
237 assert_eq!(value, json!({"PlayerStateData": {"SaveName": "MySave"}}));
238 }
239
240 #[test]
241 fn deobfuscate_array() {
242 let mapping = KeyMapping::bundled();
243 let mut value = json!({
244 "F2P": 6726,
245 "items": [
246 {"F2P": 1},
247 {"F2P": 2}
248 ]
249 });
250 mapping.deobfuscate(&mut value);
251 assert_eq!(value["Version"], 6726);
252 assert_eq!(value["items"][0]["Version"], 1);
253 assert_eq!(value["items"][1]["Version"], 2);
254 }
255
256 #[test]
257 fn deobfuscate_preserves_unknown_keys() {
258 let mapping = KeyMapping::bundled();
259 let mut value = json!({"F2P": 6726, "UnknownKey123": "hello"});
260 mapping.deobfuscate(&mut value);
261 assert_eq!(value["Version"], 6726);
262 assert_eq!(value["UnknownKey123"], "hello");
263 }
264
265 #[test]
266 fn deobfuscate_already_plaintext() {
267 let mapping = KeyMapping::bundled();
268 let mut value = json!({"Version": 6726, "PlayerStateData": {"SaveName": "MySave"}});
269 mapping.deobfuscate(&mut value);
270 assert_eq!(value["Version"], 6726);
271 assert_eq!(value["PlayerStateData"]["SaveName"], "MySave");
272 }
273
274 #[test]
275 fn savewizard_fixup() {
276 let mapping = KeyMapping::bundled();
277 let mut value = json!({"MultiTools": [1, 2, 3]});
278 mapping.deobfuscate(&mut value);
279 assert!(
280 value.get("Multitools").is_some(),
281 "savewizard fixup should rename MultiTools to Multitools"
282 );
283 assert!(value.get("MultiTools").is_none());
284 }
285
286 #[test]
287 fn is_obfuscated_with_f2p() {
288 let value = json!({"F2P": 6726, "6f=": {}});
289 assert!(is_obfuscated(&value));
290 }
291
292 #[test]
293 fn is_obfuscated_with_version() {
294 let value = json!({"Version": 6726, "PlayerStateData": {}});
295 assert!(!is_obfuscated(&value));
296 }
297
298 #[test]
299 fn is_obfuscated_no_version_key() {
300 let value = json!({"8>q": "PC"});
301 assert!(is_obfuscated(&value));
302 }
303
304 #[test]
305 fn is_obfuscated_non_object() {
306 let value = json!([1, 2, 3]);
307 assert!(!is_obfuscated(&value));
308 }
309
310 #[test]
311 fn is_obfuscated_empty_object() {
312 let value = json!({});
313 assert!(!is_obfuscated(&value));
314 }
315
316 #[test]
317 fn deobfuscate_json_bytes() {
318 let mapping = KeyMapping::bundled();
319 let json_bytes = br#"{"F2P": 6726}"#;
320 let value = deobfuscate_json(json_bytes, &mapping).unwrap();
321 assert_eq!(value["Version"], 6726);
322 }
323
324 #[test]
325 fn deobfuscate_json_already_plaintext() {
326 let mapping = KeyMapping::bundled();
327 let json_bytes = br#"{"Version": 6726}"#;
328 let value = deobfuscate_json(json_bytes, &mapping).unwrap();
329 assert_eq!(value["Version"], 6726);
330 }
331
332 #[test]
333 fn deobfuscate_json_invalid_json() {
334 let mapping = KeyMapping::bundled();
335 let json_bytes = b"not json";
336 let err = deobfuscate_json(json_bytes, &mapping).unwrap_err();
337 assert!(matches!(err, SaveError::JsonParseError { .. }));
338 }
339
340 #[test]
341 fn from_json_valid() {
342 let json = r#"{"libMBIN_version":"1.0.0","Mapping":[{"Key":"abc","Value":"Alpha"}]}"#;
343 let mapping = KeyMapping::from_json(json).unwrap();
344 assert_eq!(mapping.version, "1.0.0");
345 assert_eq!(mapping.get("abc"), Some("Alpha"));
346 assert_eq!(mapping.len(), 1);
347 }
348
349 #[test]
350 fn from_json_invalid() {
351 let json = "not valid json";
352 let err = KeyMapping::from_json(json).unwrap_err();
353 assert!(matches!(err, SaveError::MappingParseError { .. }));
354 }
355
356 #[test]
357 fn from_json_empty_mapping() {
358 let json = r#"{"libMBIN_version":"1.0.0","Mapping":[]}"#;
359 let mapping = KeyMapping::from_json(json).unwrap();
360 assert!(mapping.is_empty());
361 }
362}