Skip to content

Commit f3257ad

Browse files
committed
fix: handle lenient JSON parsing in mod detection
1 parent 91b4855 commit f3257ad

File tree

1 file changed

+69
-5
lines changed

1 file changed

+69
-5
lines changed

src-tauri/bmm-lib/src/local_mod_detection.rs

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1698,6 +1698,56 @@ fn is_likely_steamodded(path: &Path) -> Result<bool, String> {
16981698
Ok(false)
16991699
}
17001700

1701+
/// Deserialize a number field that may be an integer or float, coercing to i64.
1702+
/// Many mod authors write floats (e.g., -1000000.0 or 2.7e+27) where i64 is expected.
1703+
fn deserialize_lenient_i64<'de, D>(deserializer: D) -> Result<i64, D::Error>
1704+
where
1705+
D: serde::Deserializer<'de>,
1706+
{
1707+
use serde::de::{self, Visitor};
1708+
1709+
struct LenientI64Visitor;
1710+
1711+
impl<'de> Visitor<'de> for LenientI64Visitor {
1712+
type Value = i64;
1713+
1714+
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1715+
formatter.write_str("an integer or float")
1716+
}
1717+
1718+
fn visit_i64<E>(self, value: i64) -> Result<i64, E>
1719+
where
1720+
E: de::Error,
1721+
{
1722+
Ok(value)
1723+
}
1724+
1725+
fn visit_u64<E>(self, value: u64) -> Result<i64, E>
1726+
where
1727+
E: de::Error,
1728+
{
1729+
// Clamp to i64::MAX if too large
1730+
Ok(value.min(i64::MAX as u64) as i64)
1731+
}
1732+
1733+
fn visit_f64<E>(self, value: f64) -> Result<i64, E>
1734+
where
1735+
E: de::Error,
1736+
{
1737+
// Clamp to i64 range and convert
1738+
if value >= i64::MAX as f64 {
1739+
Ok(i64::MAX)
1740+
} else if value <= i64::MIN as f64 {
1741+
Ok(i64::MIN)
1742+
} else {
1743+
Ok(value as i64)
1744+
}
1745+
}
1746+
}
1747+
1748+
deserializer.deserialize_any(LenientI64Visitor)
1749+
}
1750+
17011751
/// JSON schema for mod configuration
17021752
#[derive(Debug, Serialize, Deserialize)]
17031753
struct ModJson {
@@ -1708,7 +1758,7 @@ struct ModJson {
17081758
description: String,
17091759
prefix: String,
17101760
main_file: String,
1711-
#[serde(default)]
1761+
#[serde(default, deserialize_with = "deserialize_lenient_i64")]
17121762
priority: i64,
17131763
#[serde(default = "default_badge_color")]
17141764
badge_colour: String,
@@ -1749,17 +1799,31 @@ fn default_text_color() -> String {
17491799
"FFFFFF".to_string()
17501800
}
17511801

1802+
/// Strip trailing commas from JSON content to handle lenient JSON authored by mod creators.
1803+
/// Matches `,` followed by optional whitespace and then `]` or `}`.
1804+
fn strip_trailing_commas(json: &str) -> String {
1805+
// Use lazy_static regex for efficiency
1806+
use regex::Regex;
1807+
lazy_static::lazy_static! {
1808+
static ref TRAILING_COMMA: Regex = Regex::new(r",(\s*[}\]])").unwrap();
1809+
}
1810+
TRAILING_COMMA.replace_all(json, "$1").into_owned()
1811+
}
1812+
17521813
/// Parse mod info from JSON file
17531814
fn parse_mod_json(json_path: &Path, mod_path: &Path) -> Result<Option<DetectedMod>, String> {
1754-
let file = match File::open(json_path) {
1755-
Ok(file) => file,
1815+
let content = match std::fs::read_to_string(json_path) {
1816+
Ok(s) => s,
17561817
Err(e) => {
1757-
log::debug!("Failed to open JSON file {}: {}", json_path.display(), e);
1818+
log::debug!("Failed to read JSON file {}: {}", json_path.display(), e);
17581819
return Ok(None);
17591820
}
17601821
};
17611822

1762-
let mod_json: ModJson = match serde_json::from_reader(file) {
1823+
// Strip trailing commas (common in hand-authored JSON)
1824+
let sanitized = strip_trailing_commas(&content);
1825+
1826+
let mod_json: ModJson = match serde_json::from_str(&sanitized) {
17631827
Ok(json) => json,
17641828
Err(e) => {
17651829
log::debug!("Failed to parse JSON file {}: {}", json_path.display(), e);

0 commit comments

Comments
 (0)