简而言之,就是检查.jar文件内的META-INF/MANIFEST.MF文件,其中的Main-Class字段具有每种核心的特征。
引言
在开发开服器时,区分服务端类型是很重要的,因为单凭文件扩展名无法判断用户提供的.jar文件究竟是什么,例如反人类的Forge核心,永远不给下载核心本体,只让下载安装器。除此之外,如果客户端核心滥竽充数,也会导致某些奇怪的问题。
然而,有一个办法可以大致上区分各种服务器核心,那就是检查.jar文件内的META-INF/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开服器中分辨其核心类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
| 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, }; } } 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) { 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; } }
|
最重要的信息是以下这段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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, };
|
比较需要注意的是,某些核心在不同的版本中会具有不同的主类名称,例如原版核心的最近几个版本(实测最晚为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库,这个库不会理会时间戳异常,因此完美规避掉了这个问题。完整实现代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
| 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, }; } } 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") { 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也同步进行了更新。问题解决!