JarUtil 嵌套抽取与版本识别
基本信息
| 属性 | 值 |
|---|---|
| 类 | com.mitchej123.jarjar.util.JarUtil |
| 关键记录 | record NestedJar(File file, String sourceFilename, String hash) |
| 父类引用 | com.mitchej123.jarjar.Config (早期配置加载) |
| Logger | NestedJarUtil |
JarUtil 是 JarJar 在 jar 文件层面的核心工具类,负责:Jar-in-Jar 抽取、版本识别(含 mcmod.info 与文件名 fallback)、coremod manifest 解析、access transformer 解析、API/earlyPackages 扫描、SortingIndex 覆写应用、嵌套 mod 解析。
NestedJar 记录
@Desugar
public record NestedJar(File file, String sourceFilename, String hash) {}
file:抽取到<mc>/nestedmods/的实际文件sourceFilename:在父 jar 内的 entry 名(保留扩展名)hash:SHA-256 十六进制字符串(用于去重与文件名生成)
getNestedJar
public static @Nullable NestedJar getNestedJar(JarFile jar, String nestedJarPath, File targetDir) {
final JarEntry nestedEntry = jar.getJarEntry(nestedJarPath);
if (nestedEntry == null) {
FMLRelaunchLog.log(Level.ERROR, "Unable to find nested jar %s in %s - ignoring", nestedJarPath, jar.getName());
return null;
}
final String nestedJarFilename = FilenameUtils.getName(nestedJarPath);
final String nestedJarName = FilenameUtils.getBaseName(nestedJarPath);
final File outputJarFile;
final String hash;
try (RewindableModInputStream is = new RewindableModInputStream(jar.getInputStream(nestedEntry))) {
hash = DigestUtils.sha256Hex(is);
synchronized (JarUtil.class) {
outputJarFile = new File(targetDir, nestedJarName + "-" + hash + ".jar");
if (!outputJarFile.exists()) {
logger.info(String.format("Extracting nested jar %s from %s to %s", nestedJarPath, jar.getName(), outputJarFile));
is.rewind();
try (final BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(outputJarFile))) {
final byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) out.write(buffer, 0, bytesRead);
}
}
}
} catch (IOException e) {
logger.error(String.format("Unable to read the nested jar %s in %s - ignoring", nestedJarPath, jar.getName()));
return null;
}
return new NestedJar(outputJarFile, nestedJarFilename, hash);
}
步骤:
jar.getJarEntry找不到 → 打 ERROR 跳过RewindableModInputStream包住流,先一次性读到 ByteBufferDigestUtils.sha256Hex算 SHA-256(不可逆;读完流已经消费)- 同步块内构造目标路径
<targetDir>/<basename>-<sha256>.jar - 不存在 →
is.rewind()后 1KB 缓冲循环写出 - 返回
NestedJar
guessVersion
public static DefaultArtifactVersion guessVersion(MetadataCollection mc, String name) {
if (mc != null) {
if (mc.modList == null) return new DefaultArtifactVersion("0.0.0");
for (ModMetadata meta : mc.modList) {
final DefaultArtifactVersion version = new DefaultArtifactVersion(meta.version);
if (!meta.version.equals(version.getQualifier())) return version;
}
}
final Matcher matcher = pattern.matcher(name);
if (matcher.matches()) return new DefaultArtifactVersion(matcher.group(1));
else return new DefaultArtifactVersion("0.0.0");
}
两个版本来源:
mcmod.info内modList[].version—— 若版本字符串能被DefaultArtifactVersion解析为非 qualifier → 用- 文件名正则
.*-([0-9]+\.[0-9]+\.[0-9]+(?:-[a-zA-Z0-9]+)?(?:\+[a-zA-Z0-9]+)?).*\.jar—— 抽出 group(1) - 都失败 →
0.0.0
支持语义化版本号 + 预发布 + 构建元数据三段:
some-mod-1.2.3.jar
some-mod-1.2.3-beta.jar
some-mod-1.2.3+build.42.jar
examineModCandidate
public static @Nullable ModCandidateV2 examineModCandidate(File modFile, ModCandidateV2 parent, boolean coremodPass) {
return examineModCandidate(modFile, parent, coremodPass, false, false);
}
public static @Nullable ModCandidateV2 examineModCandidate(File modFile, ModCandidateV2 parent, boolean coremodPass, boolean isMinecraft, boolean isClasspath) {
try (final JarFile jar = new JarFile(modFile)) {
return examineJarCandidate(jar, modFile, parent, coremodPass, isMinecraft, isClasspath);
} catch (IOException ioe) {
FMLRelaunchLog.log(Level.ERROR, ioe, "Unable to read the jar file %s - ignoring", modFile.getName());
return null;
}
}
重载控制 Minecraft 标记和 classpath 标记;失败 → 返回 null(静默跳过)。
examineJarCandidate 关键步骤
public static @Nullable ModCandidateV2 examineJarCandidate(JarFile jar, File modFile, ModCandidateV2 parent, boolean coremodPass, boolean isMinecraft, boolean isClasspath) throws IOException {
if (jar.getManifest() == null && coremodPass) return null; // 无 manifest = 不可能是 coremod
final Attributes attributes = jar.getManifest() != null ? jar.getManifest().getMainAttributes() : new Attributes();
final ModCandidateV2 modCandidate = new ModCandidateV2(modFile, modFile, ContainerType.JAR, isMinecraft, isClasspath);
modCandidate.addParent(parent);
// mcmod.info
final JarEntry modInfoEntry = jar.getJarEntry("mcmod.info");
final MetadataCollection meta;
if (modInfoEntry != null) {
meta = MetadataCollection.from(jar.getInputStream(modInfoEntry), modFile.getName());
} else {
meta = MetadataCollection.from(null, "");
}
modCandidate.setMetadataCollection(meta);
final DefaultArtifactVersion version = guessVersion(meta, modFile.getName());
modCandidate.setVersion(version);
if (coremodPass) {
modCandidate.setNestedModcandidates(checkNestedMods(jar, modCandidate, coremodPass));
} else {
modCandidate.setNestedJars(checkNestedJars(jar));
}
// FMLAT 解析
final String atList = attributes.getValue("FMLAT");
if (atList != null && coremodPass) {
final Map<String, String> modAccessTransformers = new HashMap<>();
for (String at : atList.split(" ")) {
final JarEntry jarEntry = jar.getJarEntry("META-INF/" + at);
if (jarEntry == null) continue;
modAccessTransformers.put(
String.format("%s!META-INF/%s", jar.getName(), at),
new JarByteSource(jar, jarEntry).asCharSource(Charsets.UTF_8).read());
}
modCandidate.setAccessTransformers(modAccessTransformers);
}
// TweakClass
final String cascadedTweaker = attributes.getValue("TweakClass");
if (cascadedTweaker != null) {
final int sortOrder = Optional.ofNullable(Ints.tryParse(Strings.nullToEmpty(attributes.getValue("TweakOrder")))).orElse(0);
modCandidate.setTweaker(cascadedTweaker).setSortOrder(sortOrder);
}
// MODTYPE / MODSIDE 过滤
final List<String> modTypes = attributes.containsKey(CoreModManager.MODTYPE) ? Arrays.asList(attributes.getValue(CoreModManager.MODTYPE).split(",")) : ImmutableList.of("FML");
if (!modTypes.contains("FML") && (!modCandidate.hasTweaker() || !coremodPass)) {
FMLRelaunchLog.fine("Adding %s to the list of things to skip. It is not an FML mod, it has types %s", modFile.getName(), modTypes);
if (coremodPass) addLoadedCoremod(modFile.getName());
return null;
}
final String modSide = attributes.containsKey(CoreModManager.MODSIDE) ? attributes.getValue(CoreModManager.MODSIDE) : "BOTH";
if (!("BOTH".equals(modSide) || FMLLaunchHandler.side.name().equals(modSide))) {
FMLRelaunchLog.fine("Mod %s has ModSide meta-inf value %s, and we're %s. It will be ignored", modFile.getName(), modSide, FMLLaunchHandler.side.name());
if (coremodPass) addLoadedCoremod(modFile.getName());
return null;
}
// FMLCorePlugin
final String fmlCorePlugin = attributes.getValue("FMLCorePlugin");
if (fmlCorePlugin != null) {
modCandidate.setCoreMod(fmlCorePlugin)
.setContainsMod(attributes.containsKey(CoreModManager.COREMODCONTAINSFMLMOD) || "true".equalsIgnoreCase(attributes.getValue(CoreModManagerV2.FORCELOADASMOD)));
}
if (coremodPass) applyConfiguredSortOrder(modCandidate);
if (coremodPass && !isMinecraft) scanPackagesAndApi(jar, modCandidate);
return modCandidate;
}
跳过条件:
coremodPass时无 manifestMODTYPE不包含FML且无 tweakerMODSIDE不匹配
FORCELOADASMOD 在 CoreModManagerV2 定义:
public static final Attributes.Name FORCELOADASMOD = new Attributes.Name("ForceLoadAsMod");
即使 jar 只有 tweaker 没有 @Mod,也会走 mod 装载路径。
scanPackagesAndApi
private static void scanPackagesAndApi(JarFile jar, ModCandidateV2 candidate) {
final Enumeration<JarEntry> entries = jar.entries();
String lastDir = null;
String lastPkg = null;
while (entries.hasMoreElements()) {
final JarEntry entry = entries.nextElement();
if (entry.isDirectory()) continue;
final String name = entry.getName();
if (!name.endsWith(".class")) continue;
final int slash = name.lastIndexOf('/');
final String dir = slash < 0 ? "" : name.substring(0, slash);
final String pkg;
if (dir.equals(lastDir)) {
pkg = lastPkg;
} else {
pkg = dir.replace('/', '.');
candidate.addEarlyPackage(pkg);
lastDir = dir;
lastPkg = pkg;
}
if (name.endsWith("/package-info.class")) {
try {
final DefaultArtifactVersion apiVersion = readApiVersion(new JarByteSource(jar, entry).read());
if (apiVersion != null) candidate.addDeclaredApiPackage(pkg, apiVersion);
} catch (Exception e) {
FMLRelaunchLog.log(Level.WARN, e, "Failed reading @API from %s in %s", name, candidate.getFilename());
}
}
}
}
单次扫描 jar 内所有 .class 文件,对每个 unique 目录调 candidate.addEarlyPackage(pkg);遇到 package-info.class 调 readApiVersion 读 @API(apiVersion=...) 并 addDeclaredApiPackage。
lastDir/lastPkg 缓存:同一目录的多个 class 文件只算一次包。
readApiVersion
private static DefaultArtifactVersion readApiVersion(byte[] classBytes) {
final boolean[] seen = {false};
final String[] version = {null};
new ClassReader(classBytes).accept(new ClassVisitor(Opcodes.ASM5) {
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
if (!ModCandidateV2.API_ANNOTATION_DESC.equals(desc)) return null;
seen[0] = true;
return new AnnotationVisitor(Opcodes.ASM5) {
@Override
public void visit(String n, Object value) {
if ("apiVersion".equals(n) && value instanceof String) version[0] = (String) value;
}
};
}
}, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
if (!seen[0]) return null;
return new DefaultArtifactVersion(version[0] == null || version[0].isEmpty() ? "0.0.0" : version[0]);
}
用 ASM ClassReader + ClassVisitor 只解析常量池读 @API 注解的 apiVersion 字段。
applyConfiguredSortOrder
private static void applyConfiguredSortOrder(ModCandidateV2 candidate) {
final String coreMod = candidate.getCoreMod();
if (coreMod == null || !Config.enableSortingIndexOverrides) return;
if (Config.sortingIndexOverrides.containsKey(coreMod)) {
final int sortOrder = Config.sortingIndexOverrides.get(coreMod);
candidate.setSortOrder(sortOrder);
FMLRelaunchLog.log(Level.INFO, "Applying configured SortingIndex %d to coremod %s", sortOrder, coreMod);
} else if (HODGEPODGE_COREMOD.equals(coreMod) && candidate.getVersion().compareTo(HODGEPODGE_LATE_TRANSFORMER_VERSION) < 0) {
candidate.setSortOrder(HODGEPODGE_SORT_ORDER);
FMLRelaunchLog.log(Level.INFO, "Forcing SortingIndex %d for Hodgepodge (%s).", HODGEPODGE_SORT_ORDER, candidate.getVersion());
}
}
详见 Hodgepodge 排序兜底。
checkNestedMods / checkNestedJars
public static List<ModCandidateV2> checkNestedMods(JarFile jar, ModCandidateV2 parent, boolean coremodPass) throws IOException {
final List<NestedJar> nestedJars = checkNestedJars(jar);
if (nestedJars.isEmpty()) return Collections.emptyList();
final List<ModCandidateV2> nestedMods = new ArrayList<>();
for (NestedJar nestedJar : nestedJars) {
final ModCandidateV2 nestedMod = examineModCandidate(nestedJar, parent, coremodPass);
if (nestedMod == null) continue;
nestedMod.setHash(nestedJar.hash);
nestedMods.add(nestedMod);
}
return nestedMods;
}
public static List<NestedJar> checkNestedJars(JarFile jar) throws IOException {
final Manifest manifest = jar.getManifest();
if (manifest == null) return Collections.emptyList();
final String nestedJarsEntry = manifest.getMainAttributes().getValue("Jar-In-Jar");
if (nestedJarsEntry == null) return Collections.emptyList();
final List<NestedJar> nestedJars = new ArrayList<>();
final Set<String> nestedJarPaths = new HashSet<>(Arrays.asList(nestedJarsEntry.split(",")));
for (String nestedJarPath : nestedJarPaths) {
final NestedJar nestedJar = getNestedJar(jar, nestedJarPath, CoreModManagerV2.getNestedDir());
if (nestedJar == null) continue;
nestedJars.add(nestedJar);
}
return nestedJars;
}
checkNestedJars 只抽 manifest 中 Jar-In-Jar CSV 列出的路径;checkNestedMods 进一步对每个 NestedJar 递归调 examineModCandidate,递归仅走 coremodPass=true 阶段。
相关条目
- Jar-in-Jar 嵌套 jar - 调用链
- Hodgepodge 排序兜底 - applyConfiguredSortOrder
- ModCandidateV2 数据模型 - 字段写入
- JarByteSource 字节源 - 用于读 AT/package-info
- RewindableModInputStream - 用于嵌套 jar 抽取
- API 所有权排序 - declaredApiPackages 消费者