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);
}

步骤:

  1. jar.getJarEntry 找不到 → 打 ERROR 跳过
  2. RewindableModInputStream 包住流,先一次性读到 ByteBuffer
  3. DigestUtils.sha256Hex 算 SHA-256(不可逆;读完流已经消费)
  4. 同步块内构造目标路径 <targetDir>/<basename>-<sha256>.jar
  5. 不存在 → is.rewind() 后 1KB 缓冲循环写出
  6. 返回 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");
}

两个版本来源:

  1. mcmod.info 内 modList[].version —— 若版本字符串能被 DefaultArtifactVersion 解析为非 qualifier → 用
  2. 文件名正则 .*-([0-9]+\.[0-9]+\.[0-9]+(?:-[a-zA-Z0-9]+)?(?:\+[a-zA-Z0-9]+)?).*\.jar —— 抽出 group(1)
  3. 都失败 → 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 时无 manifest
  • MODTYPE 不包含 FML 且无 tweaker
  • MODSIDE 不匹配

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 阶段。

相关条目