diff --git a/crates/ltk-manager-core/src/mods/archive/metadata.rs b/crates/ltk-manager-core/src/mods/archive/metadata.rs index 07b2ba3..aedd1f7 100644 --- a/crates/ltk-manager-core/src/mods/archive/metadata.rs +++ b/crates/ltk-manager-core/src/mods/archive/metadata.rs @@ -12,9 +12,11 @@ use crate::mods::index::LibraryModEntry; use crate::mods::types::{InstalledMod, ModLayer}; use ltk_mod_project::{ModMap, ModProject, ModProjectLayer, ModTag}; use ltk_modpkg::Modpkg; +use regex::Regex; use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf}; +use std::sync::LazyLock; pub(crate) fn read_installed_mod( entry: &LibraryModEntry, @@ -82,6 +84,36 @@ pub(crate) fn load_mod_project(mod_dir: &Path) -> AppResult { serde_json::from_str(&contents).map_err(AppError::from) } +pub(crate) fn parse_fantome_info( + content: &str, +) -> Result { + static UNQUOTED_VERSION: LazyLock = LazyLock::new(|| { + Regex::new(r#"("Version"\s*:\s*)([0-9]+(?:\.[0-9A-Za-z+-]+)+)"?(\s*[,}])"#) + .expect("unquoted Fantome version regex must be valid") + }); + + let normalized = UNQUOTED_VERSION.replace(content, r#"${1}"${2}"${3}"#); + let content = normalized.as_ref(); + + match serde_json::from_str(content) { + Ok(info) => Ok(info), + Err(original_error) => { + let Ok(mut value) = serde_json::from_str::(content) else { + return Err(original_error); + }; + let Some(version) = value.get_mut("Version") else { + return Err(original_error); + }; + let serde_json::Value::Number(version_number) = version else { + return Err(original_error); + }; + + *version = serde_json::Value::String(version_number.to_string()); + serde_json::from_value(value) + } + } +} + pub(crate) fn extract_fantome_metadata(file_path: &Path, metadata_dir: &Path) -> AppResult<()> { use std::io::Read; use zip::ZipArchive; @@ -121,7 +153,7 @@ pub(crate) fn extract_fantome_metadata(file_path: &Path, metadata_dir: &Path) -> // Parse metadata let info_content = info_content.trim_start_matches('\u{feff}').trim(); - let info: ltk_fantome::FantomeInfo = serde_json::from_str(info_content) + let info = parse_fantome_info(info_content) .map_err(|e| AppError::Other(format!("Failed to parse info.json: {}", e)))?; // Build layers from Fantome info, preserving string overrides @@ -403,6 +435,40 @@ mod tests { assert!(load_mod_project(dir.path()).is_err()); } + #[test] + fn parse_fantome_info_accepts_unquoted_decimal_version() { + let content = r#"{ + "Name": "Sausage dog Naafiri", + "Author": "Author", + "Version": 1.1, + "Description": "Description", + "Tags": [], + "Champions": [], + "Maps": [], + "Layers": {} + }"#; + + let info = parse_fantome_info(content).unwrap(); + assert_eq!(info.version, "1.1"); + } + + #[test] + fn parse_fantome_info_accepts_integer_version() { + let content = r#"{ + "Name": "Test Mod", + "Author": "Author", + "Version": 2, + "Description": "Description", + "Tags": [], + "Champions": [], + "Maps": [], + "Layers": {} + }"#; + + let info = parse_fantome_info(content).unwrap(); + assert_eq!(info.version, "2"); + } + #[test] fn read_installed_mod_populates_all_fields() { let storage = tempfile::tempdir().unwrap(); diff --git a/crates/ltk-manager-core/src/mods/archive/migration.rs b/crates/ltk-manager-core/src/mods/archive/migration.rs index 610be45..2dd04aa 100644 --- a/crates/ltk-manager-core/src/mods/archive/migration.rs +++ b/crates/ltk-manager-core/src/mods/archive/migration.rs @@ -182,7 +182,7 @@ fn read_cslol_info(path: &Path) -> AppResult { let content = fs::read_to_string(path)?; let content = content.trim_start_matches('\u{feff}').trim(); - serde_json::from_str(content).map_err(AppError::Serialization) + super::metadata::parse_fantome_info(content).map_err(AppError::Serialization) } #[cfg(test)] diff --git a/crates/ltk-manager-core/src/mods/mod.rs b/crates/ltk-manager-core/src/mods/mod.rs index 09ce71b..5bc25d2 100644 --- a/crates/ltk-manager-core/src/mods/mod.rs +++ b/crates/ltk-manager-core/src/mods/mod.rs @@ -31,6 +31,7 @@ pub use analysis::categorize::{ChampionRoster, DerivedCategorization}; pub use analysis::linked_bins::{LinkedBinOffenderInfo, LinkedBinState}; pub use analysis::wad_reports::{ModWadReport, WadReportState}; pub use archive::inspect::{ModpkgInfo, inspect_modpkg_file}; +pub(crate) use archive::metadata::parse_fantome_info; pub use archive::migration::*; pub use types::{BulkInstallResult, EditModMetadataArgs, InstalledMod, LibraryFolder, Profile}; diff --git a/crates/ltk-manager-core/src/workshop/projects.rs b/crates/ltk-manager-core/src/workshop/projects.rs index 8c3433c..26e321c 100644 --- a/crates/ltk-manager-core/src/workshop/projects.rs +++ b/crates/ltk-manager-core/src/workshop/projects.rs @@ -633,7 +633,7 @@ fn read_fantome_info( } let info_content = info_content.trim_start_matches('\u{feff}').trim(); - serde_json::from_str(info_content) + crate::mods::parse_fantome_info(info_content) .map_err(|e| AppError::Fantome(format!("Failed to parse info.json: {}", e))) }