pub mod convert;
pub mod decompress;
pub mod error;
pub mod locate;
pub mod mapping;
pub mod metadata;
pub mod model;
pub mod xxtea;
pub use decompress::{SaveFormat, decompress_save, decompress_save_file, detect_format};
pub use error::SaveError;
pub use mapping::{KeyMapping, deobfuscate_json, is_obfuscated};
pub use metadata::{SaveMetadata, StorageSlot, read_metadata, verify_sha256};
pub use model::SaveRoot;
pub fn parse_save(json: &[u8]) -> Result<SaveRoot, SaveError> {
serde_json::from_slice(json).map_err(|e| SaveError::JsonParseError {
message: e.to_string(),
})
}
pub fn parse_save_file(path: &std::path::Path) -> Result<SaveRoot, SaveError> {
let raw = std::fs::read(path)?;
let decompressed = decompress_save(&raw)?;
let json_bytes = sanitize_for_json(&decompressed);
if is_obfuscated_bytes(json_bytes.as_bytes()) {
let mapping = KeyMapping::bundled();
let value = deobfuscate_json(json_bytes.as_bytes(), &mapping)?;
serde_json::from_value(value).map_err(|e| SaveError::JsonParseError {
message: e.to_string(),
})
} else {
parse_save(json_bytes.as_bytes())
}
}
fn sanitize_for_json(data: &[u8]) -> std::borrow::Cow<'_, str> {
let trimmed = match data.iter().rposition(|&b| b != 0) {
Some(pos) => &data[..=pos],
None => data,
};
String::from_utf8_lossy(trimmed)
}
fn is_obfuscated_bytes(data: &[u8]) -> bool {
let marker = b"{\"F2P\"";
let trimmed = data
.iter()
.position(|&b| !b.is_ascii_whitespace())
.map(|pos| &data[pos..])
.unwrap_or(data);
trimmed.starts_with(marker)
}