use std::fmt::Write;
use std::io::Read;
use std::path::Path;
use crate::formats::ModMetadata;
use crate::pak::PakOperations;
use super::types::{ModPhase, ModProgress, ModProgressCallback};
pub struct InfoJsonResult {
pub success: bool,
pub content: Option<String>,
pub message: String,
}
#[must_use]
pub fn generate_info_json(source_dir: &str, pak_path: &str) -> InfoJsonResult {
generate_info_json_with_progress(source_dir, pak_path, &|_| {})
}
#[must_use]
pub fn generate_info_json_with_progress(
source_dir: &str,
pak_path: &str,
progress: ModProgressCallback,
) -> InfoJsonResult {
progress(&ModProgress::with_file(
ModPhase::Validating,
0,
3,
"Finding meta.lsx",
));
let meta_lsx_content = find_and_read_meta_lsx(source_dir);
let Some(lsx_content) = meta_lsx_content else {
return InfoJsonResult {
success: false,
content: None,
message: "No meta.lsx found. Use 'maclarian mods meta' to generate one first."
.to_string(),
};
};
let metadata = crate::formats::parse_meta_lsx(&lsx_content);
if metadata.uuid.is_empty() {
return InfoJsonResult {
success: false,
content: None,
message: "meta.lsx missing UUID".to_string(),
};
}
progress(&ModProgress::with_file(
ModPhase::CalculatingHash,
1,
3,
"Calculating PAK MD5",
));
let pak_md5 = calculate_file_md5(pak_path).unwrap_or_default();
progress(&ModProgress::with_file(
ModPhase::GeneratingJson,
2,
3,
"Generating info.json",
));
let json = generate_info_json_content(&metadata, &pak_md5);
progress(&ModProgress::new(ModPhase::Complete, 3, 3));
InfoJsonResult {
success: true,
content: Some(json),
message: "Generated successfully".to_string(),
}
}
#[must_use]
pub fn find_and_read_meta_lsx(source_dir: &str) -> Option<String> {
let source_path = Path::new(source_dir);
let mods_dir = source_path.join("Mods");
if mods_dir.exists()
&& mods_dir.is_dir()
&& let Ok(entries) = std::fs::read_dir(&mods_dir)
{
for entry in entries.flatten() {
let meta_path = entry.path().join("meta.lsx");
if meta_path.exists()
&& let Ok(content) = std::fs::read_to_string(&meta_path)
{
return Some(content);
}
}
}
let direct_meta = source_path.join("meta.lsx");
if direct_meta.exists()
&& let Ok(content) = std::fs::read_to_string(&direct_meta)
{
return Some(content);
}
None
}
#[must_use]
pub fn calculate_file_md5(file_path: &str) -> Option<String> {
let mut file = std::fs::File::open(file_path).ok()?;
let mut hasher = md5::Context::new();
let mut buffer = [0u8; 8192];
loop {
let bytes_read = file.read(&mut buffer).ok()?;
if bytes_read == 0 {
break;
}
hasher.consume(&buffer[..bytes_read]);
}
let digest = hasher.compute();
let mut hex = String::with_capacity(32);
for b in digest.iter() {
let _ = write!(hex, "{b:02x}");
}
Some(hex)
}
fn generate_info_json_content(metadata: &ModMetadata, pak_md5: &str) -> String {
let version_json = match metadata.version64 {
Some(v) => format!("\"{v}\""),
None => "null".to_string(),
};
let name = escape_json_string(&metadata.name);
let folder = escape_json_string(&metadata.folder);
let author = escape_json_string(&metadata.author);
let description = escape_json_string(&metadata.description);
let now = chrono::Utc::now();
let created = now.to_rfc3339_opts(chrono::SecondsFormat::Micros, true);
let group_uuid = uuid::Uuid::new_v4().to_string();
format!(
r#"{{"Mods":[{{"Author":"{}","Name":"{}","Folder":"{}","Version":{},"Description":"{}","UUID":"{}","Created":"{}","Dependencies":[],"Group":"{}"}}],"MD5":"{}"}}"#,
author,
name,
folder,
version_json,
description,
metadata.uuid,
created,
group_uuid,
pak_md5
)
}
fn escape_json_string(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
#[must_use]
pub fn generate_info_json_from_source(
source: &Path,
progress: ModProgressCallback,
) -> InfoJsonResult {
let is_pak = source
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("pak"));
if is_pak {
generate_info_json_from_pak(source, progress)
} else {
generate_info_json_from_directory(source, progress)
}
}
fn generate_info_json_from_pak(pak_path: &Path, progress: ModProgressCallback) -> InfoJsonResult {
progress(&ModProgress::with_file(
ModPhase::Validating,
0,
3,
"Reading meta.lsx from PAK",
));
let Some(meta_lsx_content) = find_and_read_meta_lsx_from_pak(pak_path) else {
return InfoJsonResult {
success: false,
content: None,
message: "No meta.lsx found in PAK file. Use 'maclarian mods meta' to generate one first, then recreate the .pak with 'maclarian pak create'.".to_string(),
};
};
let metadata = crate::formats::parse_meta_lsx(&meta_lsx_content);
if metadata.uuid.is_empty() {
return InfoJsonResult {
success: false,
content: None,
message: "meta.lsx missing UUID".to_string(),
};
}
progress(&ModProgress::with_file(
ModPhase::CalculatingHash,
1,
3,
"Calculating PAK MD5",
));
let pak_md5 = calculate_file_md5(&pak_path.to_string_lossy()).unwrap_or_default();
progress(&ModProgress::with_file(
ModPhase::GeneratingJson,
2,
3,
"Generating info.json",
));
let json = generate_info_json_content(&metadata, &pak_md5);
progress(&ModProgress::new(ModPhase::Complete, 3, 3));
InfoJsonResult {
success: true,
content: Some(json),
message: "Generated successfully".to_string(),
}
}
fn generate_info_json_from_directory(
dir_path: &Path,
progress: ModProgressCallback,
) -> InfoJsonResult {
progress(&ModProgress::with_file(
ModPhase::Validating,
0,
3,
"Finding meta.lsx",
));
let meta_lsx_content = find_and_read_meta_lsx(&dir_path.to_string_lossy());
let Some(lsx_content) = meta_lsx_content else {
return InfoJsonResult {
success: false,
content: None,
message:
"No meta.lsx found in directory. Use 'maclarian mods meta' to generate one first."
.to_string(),
};
};
let metadata = crate::formats::parse_meta_lsx(&lsx_content);
if metadata.uuid.is_empty() {
return InfoJsonResult {
success: false,
content: None,
message: "meta.lsx missing UUID".to_string(),
};
}
progress(&ModProgress::with_file(
ModPhase::CalculatingHash,
1,
3,
"Finding and hashing PAK file",
));
let pak_md5 = find_pak_and_calculate_md5(dir_path).unwrap_or_default();
if pak_md5.is_empty() {
return InfoJsonResult {
success: false,
content: None,
message: "No .pak file found in directory".to_string(),
};
}
progress(&ModProgress::with_file(
ModPhase::GeneratingJson,
2,
3,
"Generating info.json",
));
let json = generate_info_json_content(&metadata, &pak_md5);
progress(&ModProgress::new(ModPhase::Complete, 3, 3));
InfoJsonResult {
success: true,
content: Some(json),
message: "Generated successfully".to_string(),
}
}
fn find_and_read_meta_lsx_from_pak(pak_path: &Path) -> Option<String> {
let files = PakOperations::list(pak_path).ok()?;
let meta_path = files
.iter()
.find(|f| f.to_lowercase().ends_with("meta.lsx"))?;
let bytes = PakOperations::read_file_bytes(pak_path, meta_path).ok()?;
String::from_utf8(bytes).ok()
}
fn find_pak_and_calculate_md5(dir_path: &Path) -> Option<String> {
let entries = std::fs::read_dir(dir_path).ok()?;
for entry in entries.flatten() {
let path = entry.path();
if path.is_file()
&& path
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("pak"))
{
return calculate_file_md5(&path.to_string_lossy());
}
}
None
}