feat: Enhance Minecraft version detection with JAR file analysis and improved fallback mechanisms

This commit is contained in:
Rbanh 2025-03-29 01:42:58 -04:00
parent fc96a10397
commit 4adb291592

View File

@ -11,6 +11,7 @@ use sha2::{Sha256, Digest};
use reqwest; use reqwest;
use std::error::Error; use std::error::Error;
use tauri::AppHandle; use tauri::AppHandle;
use std::path::PathBuf;
// Add the crawlers module // Add the crawlers module
mod crawlers; mod crawlers;
@ -395,115 +396,175 @@ fn detect_server_type(server_path: &Path) -> ServerType {
ServerType::Unknown ServerType::Unknown
} }
/// Helper to find the most likely server JAR file
fn find_server_jar(server_path: &Path) -> Option<PathBuf> {
const COMMON_NAMES: [&str; 4] = ["server.jar", "spigot.jar", "paper.jar", "craftbukkit.jar"];
let mut largest_jar: Option<(u64, PathBuf)> = None;
let mut found_common_name: Option<PathBuf> = None;
if let Ok(entries) = fs::read_dir(server_path) {
for entry in entries.filter_map(Result::ok) {
let path = entry.path();
if path.is_file() && path.extension().map_or(false, |ext| ext.eq_ignore_ascii_case("jar")) {
// Check for common names first
if let Some(filename) = path.file_name().and_then(|n| n.to_str()) {
if COMMON_NAMES.contains(&filename) {
found_common_name = Some(path.clone());
break; // Found a primary common name, stop looking
}
}
// Track largest JAR as a fallback
if let Ok(metadata) = entry.metadata() {
let size = metadata.len();
if largest_jar.is_none() || size > largest_jar.as_ref().unwrap().0 {
largest_jar = Some((size, path.clone()));
}
}
}
}
}
// Prioritize common names, then largest JAR
found_common_name.or_else(|| largest_jar.map(|(_, path)| path))
}
/// Helper to read version.json from inside a JAR archive
fn read_version_from_jar(jar_path: &Path) -> Option<String> {
match File::open(jar_path) {
Ok(file) => {
match ZipArchive::new(file) {
Ok(mut archive) => {
match archive.by_name("version.json") {
Ok(mut version_file) => {
let mut contents = String::new();
if version_file.read_to_string(&mut contents).is_ok() {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&contents) {
if let Some(version) = json["name"].as_str() {
return Some(version.to_string());
}
}
}
}
Err(_) => { /* version.json not found in archive */ }
}
}
Err(e) => println!("Failed to read JAR archive {}: {}", jar_path.display(), e),
}
}
Err(e) => println!("Failed to open JAR file {}: {}", jar_path.display(), e),
}
None
}
/// Guess the Minecraft version from various files in the server directory /// Guess the Minecraft version from various files in the server directory
fn detect_minecraft_version(server_path: &Path, server_type: &ServerType) -> Option<String> { fn detect_minecraft_version(server_path: &Path, server_type: &ServerType) -> Option<String> {
// Try from version.json if it exists // 1. Try external version.json
if let Ok(content) = fs::read_to_string(server_path.join("version.json")) { if let Ok(content) = fs::read_to_string(server_path.join("version.json")) {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) { if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
if let Some(version) = json["name"].as_str() { if let Some(version) = json["name"].as_str() {
println!("Version found via external version.json");
return Some(version.to_string()); return Some(version.to_string());
} }
} }
} }
// --- Try parsing paper-global.yml for Paper servers --- // 2. Try parsing paper-global.yml for Paper servers
if server_type == &ServerType::Paper { if server_type == &ServerType::Paper {
let paper_global_path = server_path.join("config").join("paper-global.yml"); let paper_global_path = server_path.join("config").join("paper-global.yml");
if paper_global_path.exists() { if paper_global_path.exists() {
if let Ok(content) = fs::read_to_string(&paper_global_path) { if let Ok(content) = fs::read_to_string(&paper_global_path) {
// Use yaml_rust::YamlLoader here
match yaml_rust::YamlLoader::load_from_str(&content) { match yaml_rust::YamlLoader::load_from_str(&content) {
Ok(docs) if !docs.is_empty() => { Ok(docs) if !docs.is_empty() => {
let doc = &docs[0]; let doc = &docs[0];
// Common location for MC version, might differ
if let Some(version) = doc["misc"]["paper-version"].as_str() { if let Some(version) = doc["misc"]["paper-version"].as_str() {
// Often includes build number, try to extract base MC version
let mc_version = version.split('-').next().unwrap_or(version); let mc_version = version.split('-').next().unwrap_or(version);
println!("Version found via paper-global.yml (misc.paper-version)");
return Some(mc_version.to_string()); return Some(mc_version.to_string());
} }
// Fallback check, some older versions might store it differently
if let Some(version) = doc["settings"]["minecraft-version"].as_str() { if let Some(version) = doc["settings"]["minecraft-version"].as_str() {
println!("Version found via paper-global.yml (settings.minecraft-version)");
return Some(version.to_string()); return Some(version.to_string());
} }
} }
Err(e) => { Err(e) => println!("Failed to parse paper-global.yml: {}", e),
println!("Failed to parse paper-global.yml: {}", e);
}
_ => { /* Empty or invalid YAML */ } _ => { /* Empty or invalid YAML */ }
} }
} }
} }
} }
// --- End Paper version check ---
// Try from the server jar name pattern // 3. Try reading version.json from inside the server JAR
if let Ok(entries) = fs::read_dir(server_path) { if let Some(server_jar_path) = find_server_jar(server_path) {
for entry in entries { if let Some(version) = read_version_from_jar(&server_jar_path) {
if let Ok(entry) = entry { println!("Version found via internal version.json in {}", server_jar_path.display());
let path = entry.path(); return Some(version);
if path.is_file() && path.extension().map_or(false, |ext| ext == "jar") {
let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
// Extract version from various common patterns in jar names
if filename.starts_with("paper-") ||
filename.starts_with("spigot-") ||
filename.starts_with("craftbukkit-") {
// Pattern: paper-1.19.2.jar, spigot-1.19.2.jar
let parts: Vec<&str> = filename.split('-').collect();
if parts.len() > 1 {
let version_part = parts[1].trim_end_matches(".jar");
if version_part.contains('.') { // Basic version format check
return Some(version_part.to_string());
}
}
}
// Look for version patterns like minecraft_server.1.19.2.jar
if filename.starts_with("minecraft_server.") {
let version_part = filename
.trim_start_matches("minecraft_server.")
.trim_end_matches(".jar");
if version_part.contains('.') {
return Some(version_part.to_string());
}
}
}
}
} }
} }
// If server type is proxy, look in config files // 4. Try fallback: JAR filename pattern matching
if let Ok(entries) = fs::read_dir(server_path) {
for entry in entries {
if let Ok(entry) = entry {
let path = entry.path();
if path.is_file() && path.extension().map_or(false, |ext| ext == "jar") {
let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
if filename.starts_with("paper-") || filename.starts_with("spigot-") || filename.starts_with("craftbukkit-") {
let parts: Vec<&str> = filename.split('-').collect();
if parts.len() > 1 {
let version_part = parts[1].trim_end_matches(".jar");
if version_part.contains('.') { // Basic version format check
println!("Version inferred from JAR filename pattern: {}", filename);
return Some(version_part.to_string());
}
}
}
if filename.starts_with("minecraft_server.") {
let version_part = filename.trim_start_matches("minecraft_server.").trim_end_matches(".jar");
if version_part.contains('.') {
println!("Version inferred from minecraft_server JAR filename: {}", filename);
return Some(version_part.to_string());
}
}
}
}
}
}
// 5. Try fallback: Proxy config files (Velocity, Bungee, Waterfall)
if server_type == &ServerType::BungeeCord || if server_type == &ServerType::BungeeCord ||
server_type == &ServerType::Waterfall || server_type == &ServerType::Waterfall ||
server_type == &ServerType::Velocity { server_type == &ServerType::Velocity {
// Velocity uses TOML, others use YAML // ... (existing proxy config logic remains here) ...
if server_type == &ServerType::Velocity { if server_type == &ServerType::Velocity {
if let Ok(content) = fs::read_to_string(server_path.join("velocity.toml")) { // ... velocity.toml check ...
// Very basic TOML parsing just for this field if let Ok(content) = fs::read_to_string(server_path.join("velocity.toml")) {
for line in content.lines() { for line in content.lines() {
if line.contains("minecraft-version") { if line.contains("minecraft-version") {
if let Some(version) = line.split('=').nth(1) { if let Some(version) = line.split('=').nth(1) {
return Some(version.trim().trim_matches('"').to_string()); println!("Version found via velocity.toml");
} return Some(version.trim().trim_matches('"').to_string());
} }
} }
} }
} else { }
// Try to parse config.yml for BungeeCord/Waterfall } else {
if let Ok(content) = fs::read_to_string(server_path.join("config.yml")) { // ... config.yml check ...
if let Ok(docs) = YamlLoader::load_from_str(&content) { if let Ok(content) = fs::read_to_string(server_path.join("config.yml")) {
if !docs.is_empty() { if let Ok(docs) = YamlLoader::load_from_str(&content) {
let doc = &docs[0]; if !docs.is_empty() {
if let Some(version) = doc["minecraft_version"].as_str() { let doc = &docs[0];
return Some(version.to_string()); if let Some(version) = doc["minecraft_version"].as_str() {
} println!("Version found via config.yml");
} return Some(version.to_string());
} }
} }
} }
}
}
} }
// Default fallback // 6. No version found
println!("Could not detect Minecraft version.");
None None
} }
@ -959,3 +1020,5 @@ pub fn run() {
.expect("error while running tauri application"); .expect("error while running tauri application");
} }