266 lines
9.1 KiB
C++
266 lines
9.1 KiB
C++
#include "javaruntime.h"
|
|
#include "localization.h"
|
|
|
|
#include "launcherpaths.h"
|
|
|
|
#include <QDir>
|
|
#include <QDirIterator>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QRegularExpression>
|
|
#include <QSaveFile>
|
|
|
|
#include <algorithm>
|
|
|
|
namespace {
|
|
|
|
#if defined(Q_OS_WIN)
|
|
constexpr auto kExecutable = "java.exe";
|
|
#else
|
|
constexpr auto kExecutable = "java";
|
|
#endif
|
|
|
|
const QString kMetaFile = QStringLiteral("runtime.json");
|
|
|
|
} // 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<JavaRuntimeKind> 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 Loc::text("common.java.kind.java");
|
|
case JavaRuntimeKind::Jdk: return Loc::text("common.java.kind.jdk");
|
|
case JavaRuntimeKind::Jre: return Loc::text("common.java.kind.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<InstalledJavaRuntime> 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<qint64>(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<InstalledJavaRuntime> JavaRuntimeStore::installed()
|
|
{
|
|
QList<InstalledJavaRuntime> 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 = Loc::text("java.error.invalidRuntimeId");
|
|
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<double>(runtime.size)}};
|
|
|
|
QSaveFile file(dir + u'/' + kMetaFile);
|
|
if (!file.open(QIODevice::WriteOnly)) {
|
|
if (error)
|
|
*error = Loc::text("java.error.writeFailed").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 = Loc::text("java.error.writeFailed").arg(QDir::toNativeSeparators(file.fileName()));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool JavaRuntimeStore::remove(const QString &id, QString *error)
|
|
{
|
|
const QString dir = dirFor(id);
|
|
// Удаляем только внутри <root>/java: sanitizeId не пропустит «..», но
|
|
// проверка стоит одной строки, а цена ошибки — чужая папка.
|
|
if (dir.isEmpty() || !dir.startsWith(LauncherPaths::javaDir() + u'/')) {
|
|
if (error)
|
|
*error = Loc::text("java.error.invalidRuntimeId");
|
|
return false;
|
|
}
|
|
if (!QDir(dir).exists())
|
|
return true;
|
|
if (!QDir(dir).removeRecursively()) {
|
|
if (error)
|
|
*error = Loc::text("common.error.removeFailed").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 Loc::text("java.range.modern");
|
|
if (javaMajor >= 17)
|
|
return Loc::text("java.range.mid");
|
|
if (javaMajor == 16)
|
|
return Loc::text("java.range.seventeen");
|
|
if (javaMajor >= 9)
|
|
return Loc::text("java.range.unsuitable");
|
|
if (javaMajor > 0)
|
|
return Loc::text("java.range.legacy");
|
|
return {};
|
|
}
|