Files

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