#include "javaruntime.h" #include "launcherpaths.h" #include #include #include #include #include #include #include #include #include #include namespace { #if defined(Q_OS_WIN) constexpr auto kExecutable = "java.exe"; #else constexpr auto kExecutable = "java"; #endif const QString kMetaFile = QStringLiteral("runtime.json"); QString translate(const char *text) { return QCoreApplication::translate("JavaRuntime", text); } } // namespace QString javaKindKey(JavaRuntimeKind kind) { switch (kind) { case JavaRuntimeKind::Mojang: return QStringLiteral("java"); case JavaRuntimeKind::Jdk: return QStringLiteral("jdk"); case JavaRuntimeKind::Jre: return QStringLiteral("jre"); } return QStringLiteral("jdk"); } std::optional javaKindFromKey(const QString &key) { if (key == QLatin1String("java")) return JavaRuntimeKind::Mojang; if (key == QLatin1String("jdk")) return JavaRuntimeKind::Jdk; if (key == QLatin1String("jre")) return JavaRuntimeKind::Jre; return std::nullopt; } QString javaKindTitle(JavaRuntimeKind kind) { switch (kind) { case JavaRuntimeKind::Mojang: return translate("Java"); case JavaRuntimeKind::Jdk: return translate("JDK"); case JavaRuntimeKind::Jre: return translate("JRE"); } return {}; } QString JavaRuntimeStore::sanitizeId(const QString &id) { QString out; out.reserve(id.size()); for (const QChar ch : id) { const bool safe = ch.isLetterOrNumber() || ch == u'.' || ch == u'-' || ch == u'_'; out += safe ? ch : u'_'; } // Точки в начале превратили бы папку в скрытую, а «..» — в путь наружу. while (out.startsWith(u'.')) out.remove(0, 1); return out; } QString JavaRuntimeStore::dirFor(const QString &id) { const QString safe = sanitizeId(id); if (safe.isEmpty()) return {}; return LauncherPaths::javaDir() + u'/' + safe; } QString JavaRuntimeStore::locateBinary(const QString &rootDir) { if (rootDir.isEmpty() || !QDir(rootDir).exists()) return {}; // Кандидатов бывает несколько: в JDK рядом с bin/java лежит ещё и // jre/bin/java. Берём самый короткий путь — это корневой интерпретатор. QString best; QDirIterator it(rootDir, QStringList{QString::fromLatin1(kExecutable)}, QDir::Files, QDirIterator::Subdirectories); while (it.hasNext()) { it.next(); const QFileInfo info = it.fileInfo(); if (info.absolutePath().section(u'/', -1) != QLatin1String("bin")) continue; const QString path = info.absoluteFilePath(); if (best.isEmpty() || path.length() < best.length()) best = path; } return best; } std::optional JavaRuntimeStore::find(const QString &id) { const QString dir = dirFor(id); if (dir.isEmpty()) return std::nullopt; QFile file(dir + u'/' + kMetaFile); if (!file.open(QIODevice::ReadOnly)) return std::nullopt; const QJsonObject object = QJsonDocument::fromJson(file.readAll()).object(); InstalledJavaRuntime runtime; runtime.id = object.value(QStringLiteral("id")).toString(); runtime.kind = object.value(QStringLiteral("kind")).toString(); runtime.major = object.value(QStringLiteral("major")).toInt(); runtime.version = object.value(QStringLiteral("version")).toString(); runtime.size = static_cast(object.value(QStringLiteral("size")).toDouble()); // Путь хранится относительным: папку лаунчера могли перенести вместе с // профилем пользователя, а абсолютный путь этого не переживает. const QString relative = object.value(QStringLiteral("javaPath")).toString(); runtime.javaPath = relative.isEmpty() ? QString() : QDir(dir).absoluteFilePath(relative); if (runtime.id != id || runtime.javaPath.isEmpty() || !QFileInfo::exists(runtime.javaPath)) return std::nullopt; return runtime; } QList JavaRuntimeStore::installed() { QList result; const QDir root(LauncherPaths::javaDir()); const QStringList entries = root.entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name); for (const QString &entry : entries) { const auto runtime = find(entry); if (runtime) result << *runtime; } std::stable_sort(result.begin(), result.end(), [](const InstalledJavaRuntime &a, const InstalledJavaRuntime &b) { if (a.major != b.major) return a.major > b.major; return a.version > b.version; }); return result; } bool JavaRuntimeStore::writeMeta(const InstalledJavaRuntime &runtime, QString *error) { const QString dir = dirFor(runtime.id); if (dir.isEmpty()) { if (error) *error = translate("недопустимый идентификатор сборки Java"); return false; } const QJsonObject object{ {QStringLiteral("id"), runtime.id}, {QStringLiteral("kind"), runtime.kind}, {QStringLiteral("major"), runtime.major}, {QStringLiteral("version"), runtime.version}, {QStringLiteral("javaPath"), QDir(dir).relativeFilePath(runtime.javaPath)}, {QStringLiteral("size"), static_cast(runtime.size)}}; QSaveFile file(dir + u'/' + kMetaFile); if (!file.open(QIODevice::WriteOnly)) { if (error) *error = translate("не удалось записать %1").arg(QDir::toNativeSeparators(file.fileName())); return false; } const QByteArray payload = QJsonDocument(object).toJson(QJsonDocument::Compact); if (file.write(payload) != payload.size() || !file.commit()) { file.cancelWriting(); if (error) *error = translate("не удалось записать %1").arg(QDir::toNativeSeparators(file.fileName())); return false; } return true; } bool JavaRuntimeStore::remove(const QString &id, QString *error) { const QString dir = dirFor(id); // Удаляем только внутри /java: sanitizeId не пропустит «..», но // проверка стоит одной строки, а цена ошибки — чужая папка. if (dir.isEmpty() || !dir.startsWith(LauncherPaths::javaDir() + u'/')) { if (error) *error = translate("недопустимый идентификатор сборки Java"); return false; } if (!QDir(dir).exists()) return true; if (!QDir(dir).removeRecursively()) { if (error) *error = translate("не удалось удалить %1").arg(QDir::toNativeSeparators(dir)); return false; } return true; } int JavaRequirement::minimumFor(const QString &minecraftVersionId) { const QString id = minecraftVersionId.trimmed(); if (id.isEmpty()) return 0; // Снапшоты — YYwWWa. Ровных границ по неделям нет, но год отвечает на // вопрос «какая Java» точнее, чем ничего: 1.17 (Java 16) вышла в 21-м году, // 1.18 (Java 17) в конце того же года, 1.20.5 (Java 21) — в 24-м. static const QRegularExpression snapshot(QStringLiteral("^(\\d{2})w\\d{2}[a-z]$")); const QRegularExpressionMatch snapshotMatch = snapshot.match(id); if (snapshotMatch.hasMatch()) { const int year = snapshotMatch.captured(1).toInt(); if (year >= 24) return 21; if (year >= 22) return 17; if (year == 21) return 16; return 8; } // Релизы и их пре-релизы: 1.20.5, 1.21, 1.21.4-pre1, 26.2. static const QRegularExpression release( QStringLiteral("^(\\d+)(?:\\.(\\d+))?(?:\\.(\\d+))?")); const QRegularExpressionMatch releaseMatch = release.match(id); if (!releaseMatch.hasMatch()) return 8; // a1.2.6, b1.7.3, rd-132211 и прочая доисторическая ваниль const int epoch = releaseMatch.captured(1).toInt(); const int major = releaseMatch.captured(2).toInt(); const int patch = releaseMatch.captured(3).toInt(); // Нумерация после 1.21.x пошла по годам (26.2) — это заведомо новее 1.20.5. if (epoch != 1) return 21; if (major >= 21) return 21; if (major == 20) return patch >= 5 ? 21 : 17; if (major >= 18) return 17; if (major == 17) return 16; return 8; } QString JavaRequirement::coverage(int javaMajor) { if (javaMajor >= 21) return translate("Minecraft 1.20.5 и новее"); if (javaMajor >= 17) return translate("Minecraft 1.18 — 1.20.4"); if (javaMajor == 16) return translate("Minecraft 1.17 — 1.17.1"); if (javaMajor >= 9) return translate("для Minecraft не подходит"); if (javaMajor > 0) return translate("Minecraft 1.16.5 и старше"); return {}; }