简而言之,就是检查.jar文件内的META-INF/MANIFEST.MF文件,其中的Main-Class字段具有每种核心的特征。
引言
在开发开服器时,区分服务端类型是很重要的,因为单凭文件扩展名无法判断用户提供的.jar文件究竟是什么,例如反人类的Forge核心,永远不给下载核心本体,只让下载安装器。除此之外,如果客户端核心滥竽充数,也会导致某些奇怪的问题。
然而,有一个办法可以大致上区分各种服务器核心,那就是检查.jar文件内的META-INF/MANIFEST.MF文件。
关于MANIFEST.MF
MANIFEST.MF文件是Java平台的一种规范,用于定义和管理Java应用程序的组件、库和模块。它是JAR文件中的一个纯文本文件,遵循特定的格式规范。
在JAR文件中,MANIFEST.MF文件必须位于META-INF目录下,且一个JAR文件中只能有一个MANIFEST.MF文件。
在MANIFEST.MF文件中,Main-Class字段向Java虚拟机指明了该文件的主类,以使jar文件能够正常执行。在较新的MC版本中都会具有Main-Class字段(目前已知的只有远古版本没有)。由于不同的核心都会有自己的主类名称,因此可以通过这个特征在MC开服器中分辨其核心类型。
实现
jar文件本质上就是一个zip文件,因此我们可以直接通过解压zip文件的方式来获取jar文件的内容,并读取MANIFEST.MF文件。以下是C#代码示例(来自LSL v0.08):
public class CoreValidationService
{
public enum CoreType// 核心类型的枚举
{
Error,
Unknown,
Client,
ForgeInstaller,
FabricInstaller,
Forge,
Fabric,
Arclight,
CatServer,
CraftBukkit,
Leaves,
LightFall,
Mohist,
Paper,
Vanilla,
Velocity,
}
public static CoreType Validate(string? filePath, out string ErrorMessage)// 校验核心类型
{
ErrorMessage = "";
if (string.IsNullOrEmpty(filePath))
{
ErrorMessage = "选定的路径为空";
return CoreType.Error;
}
if (!File.Exists(filePath))
{
ErrorMessage = "选定的文件/路径不存在";
return CoreType.Error;
}
string? JarMainClass = GetMainClass(filePath);
if (JarMainClass == null) return CoreType.Unknown;
else if (JarMainClass.StartsWith("Access denied") || JarMainClass.StartsWith("Error"))
{
ErrorMessage = JarMainClass;
return CoreType.Error;
}
else
{
return JarMainClass switch
{
"net.minecraft.server.MinecraftServer" => CoreType.Vanilla,
"net.minecraft.bundler.Main" => CoreType.Vanilla,
"net.minecraft.client.Main" => CoreType.Client,
"net.minecraftforge.installer.SimpleInstaller" => CoreType.ForgeInstaller,
"net.fabricmc.installer.Main" => CoreType.FabricInstaller,
"net.fabricmc.installer.ServerLauncher" => CoreType.Fabric,
"io.izzel.arclight.server.Launcher" => CoreType.Arclight,
"catserver.server.CatServerLaunch" => CoreType.CatServer,
"foxlaunch.FoxServerLauncher" => CoreType.CatServer,
"org.bukkit.craftbukkit.Main" => CoreType.CraftBukkit,
"org.bukkit.craftbukkit.bootstrap.Main" => CoreType.CraftBukkit,
"io.papermc.paperclip.Main" => CoreType.Paper,
"org.leavesmc.leavesclip.Main" => CoreType.Leaves,
"net.md_5.bungee.Bootstrap" => CoreType.LightFall,
"com.mohistmc.MohistMCStart" => CoreType.Mohist,
"com.mohistmc.MohistMC" => CoreType.Mohist,
"com.destroystokyo.paperclip.Paperclip" => CoreType.Paper,
"com.velocitypowered.proxy.Velocity" => CoreType.Velocity,
_ => CoreType.Unknown,
};
}
}
// the following code is taken from https://github.com/Orange-Icepop/JavaMainClassFinder
public static string? GetMainClass(string jarFilePath)
{
try
{
using FileStream stream = new FileStream(jarFilePath, FileMode.Open);
using ZipArchive archive = new ZipArchive(stream, ZipArchiveMode.Read);
ZipArchiveEntry? manifestEntry = archive.Entries.FirstOrDefault(entry => entry.FullName == "META-INF/MANIFEST.MF");
if (manifestEntry != null)// 对于较新版本的MC,MANIFEST.MF中应当包含Main-Class字段
{
using StreamReader reader = new StreamReader(manifestEntry.Open());
string manifestContent = reader.ReadToEnd();
return FindMainClassLine(manifestContent);
}
return null;
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("Access denied: " + ex.Message);
return "Access denied: " + ex.Message;
}
catch (IOException ex)
{
Console.WriteLine("IO error: " + ex.Message);
return "Error reading file: " + ex.Message;
}
catch (Exception ex)
{
Console.WriteLine("Error reading jar file: " + ex.Message);
return "Error reading jar file: " + ex.Message;
}
}
public static string? FindMainClassLine(string manifestContent)
{
string[] lines = manifestContent.Split(["\r\n", "\r", "\n"], StringSplitOptions.None);
foreach (string line in lines)
{
if (line.StartsWith("Main-Class:"))
{
return line.Substring("Main-Class:".Length).Trim();
}
}
return null;
}
}
最重要的信息是以下这段:
return JarMainClass switch//使用switch表达式返回值
{
"net.minecraft.server.MinecraftServer" => CoreType.Vanilla,
"net.minecraft.bundler.Main" => CoreType.Vanilla,
"net.minecraft.client.Main" => CoreType.Client,
"net.minecraftforge.installer.SimpleInstaller" => CoreType.ForgeInstaller,
"net.fabricmc.installer.Main" => CoreType.FabricInstaller,
"net.fabricmc.installer.ServerLauncher" => CoreType.Fabric,
"io.izzel.arclight.server.Launcher" => CoreType.Arclight,
"catserver.server.CatServerLaunch" => CoreType.CatServer,
"foxlaunch.FoxServerLauncher" => CoreType.CatServer,
"org.bukkit.craftbukkit.Main" => CoreType.CraftBukkit,
"org.bukkit.craftbukkit.bootstrap.Main" => CoreType.CraftBukkit,
"io.papermc.paperclip.Main" => CoreType.Paper,
"org.leavesmc.leavesclip.Main" => CoreType.Leaves,
"net.md_5.bungee.Bootstrap" => CoreType.LightFall,
"com.mohistmc.MohistMCStart" => CoreType.Mohist,
"com.mohistmc.MohistMC" => CoreType.Mohist,
"com.destroystokyo.paperclip.Paperclip" => CoreType.Paper,
"com.velocitypowered.proxy.Velocity" => CoreType.Velocity,
_ => CoreType.Unknown,
};
比较需要注意的是,某些核心在不同的版本中会具有不同的主类名称,例如原版核心的最近几个版本(实测最晚为1.21)的主类名称从net.minecraft.server.MinecraftServer转为了net.minecraft.bundler.Main,但是客户端的主类没改。
还有一部分核心由于底层相同,并没有修改主类名称,例如Folia和Paper的主类名称就相同。
部分核心的问题
之前的代码来自LSL v0.08是有原因的。在0.08之后,我对所有类型的核心进行了测试,发现Mohist核心与Folia核心会在解析时抛出一大堆的ArgumentOutOfRangeException异常。经过查询,这是在解析时间戳时出现的问题,非常有可能是因为这些核心的时间戳异常导致的。当然,这不影响核心文件的执行,但是对于上面我们依赖的System.IO.Compression命名空间中的方法具有致命的执行效率打击,也就是在0.08版本和0.07.1版本中添加某些核心时会导致整个应用程序卡死的原因。
为了解决这个问题,我在0.08.1版本中引入了SharpZipLib库,这个库不会理会时间戳异常,因此完美规避掉了这个问题。完整实现代码如下:
using ICSharpCode.SharpZipLib.Zip;
using System;
using System.IO;
namespace LSL.Services.Validators
{
public class CoreValidationService
{
public enum CoreType
{
Error,
Unknown,
Client,
ForgeInstaller,
FabricInstaller,
Forge,
Fabric,
Arclight,
CatServer,
CraftBukkit,
Leaves,
LightFall,
Mohist,
Paper,
Vanilla,
Velocity,
}
public static CoreType Validate(string? filePath, out string ErrorMessage)// 校验核心类型
{
ErrorMessage = "";
if (string.IsNullOrEmpty(filePath))
{
ErrorMessage = "选定的路径为空";
return CoreType.Error;
}
if (!File.Exists(filePath))
{
ErrorMessage = "选定的文件/路径不存在";
return CoreType.Error;
}
string? JarMainClass = GetMainClass(filePath);
if (JarMainClass == null) return CoreType.Unknown;
else if (JarMainClass.StartsWith("Access denied") || JarMainClass.StartsWith("Error"))
{
ErrorMessage = JarMainClass;
return CoreType.Error;
}
else
{
return JarMainClass switch
{
"net.minecraft.server.MinecraftServer" => CoreType.Vanilla,
"net.minecraft.bundler.Main" => CoreType.Vanilla,
"net.minecraft.client.Main" => CoreType.Client,
"net.minecraftforge.installer.SimpleInstaller" => CoreType.ForgeInstaller,
"net.fabricmc.installer.Main" => CoreType.FabricInstaller,
"net.fabricmc.installer.ServerLauncher" => CoreType.Fabric,
"io.izzel.arclight.server.Launcher" => CoreType.Arclight,
"catserver.server.CatServerLaunch" => CoreType.CatServer,
"foxlaunch.FoxServerLauncher" => CoreType.CatServer,
"org.bukkit.craftbukkit.Main" => CoreType.CraftBukkit,
"org.bukkit.craftbukkit.bootstrap.Main" => CoreType.CraftBukkit,
"io.papermc.paperclip.Main" => CoreType.Paper,
"org.leavesmc.leavesclip.Main" => CoreType.Leaves,
"net.md_5.bungee.Bootstrap" => CoreType.LightFall,
"com.mohistmc.MohistMCStart" => CoreType.Mohist,
"com.mohistmc.MohistMC" => CoreType.Mohist,
"com.destroystokyo.paperclip.Paperclip" => CoreType.Paper,
"com.velocitypowered.proxy.Velocity" => CoreType.Velocity,
_ => CoreType.Unknown,
};
}
}
// the following code is taken from https://github.com/Orange-Icepop/JavaMainClassFinder
public static string? GetMainClass(string jarFilePath)
{
try
{
using FileStream stream = new FileStream(jarFilePath, FileMode.Open);
using (ZipFile zipFile = new ZipFile(stream))
{
foreach (ZipEntry entry in zipFile)
{
if (entry.IsDirectory)
continue;
if (entry.Name == "META-INF/MANIFEST.MF")// 对于较新版本的MC,MANIFEST.MF中应当包含Main-Class字段
{
using (var fstream = zipFile.GetInputStream(entry))
using (StreamReader reader = new StreamReader(fstream))
{
string manifestContent = reader.ReadToEnd();
return FindMainClassLine(manifestContent);
}
}
}
}
return null;
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("Access denied: " + ex.Message);
return "Access denied: " + ex.Message;
}
catch (IOException ex)
{
Console.WriteLine("IO error: " + ex.Message);
return "Error reading file: " + ex.Message;
}
catch (Exception ex)
{
Console.WriteLine("Error reading jar file: " + ex.Message);
return "Error reading jar file: " + ex.Message;
}
}
public static string? FindMainClassLine(string manifestContent)
{
string[] lines = manifestContent.Split(["\r\n", "\r", "\n"], StringSplitOptions.None);
foreach (string line in lines)
{
if (line.StartsWith("Main-Class:"))
{
return line.Substring("Main-Class:".Length).Trim();
}
}
return null;
}
}
}
JavaMainClassFinder也同步进行了更新。问题解决!