cleanups
This commit is contained in:
@@ -20,27 +20,18 @@
|
||||
|
||||
namespace {
|
||||
|
||||
// Формат каталога, который понимает этот лаунчер. Сервер, отдающий версию
|
||||
// новее, обслуживает лаунчер новее — молча показывать половину полей нельзя.
|
||||
constexpr int kSupportedFormatVersion = 1;
|
||||
|
||||
// Каталог весит килобайты, а новая ревизия сборки должна доезжать до игроков
|
||||
// быстро. Четверть часа — это и не поток запросов при каждом открытии окна, и
|
||||
// не полдня ожидания после публикации; кнопка «Обновить список» ходит с force.
|
||||
constexpr qint64 kCacheTtlSeconds = 15 * 60;
|
||||
|
||||
constexpr int kTransferTimeoutMs = 20000;
|
||||
|
||||
// id попадает в имена файлов и в сравнение с локальными записями, поэтому
|
||||
// набор символов узкий и проверяется до всего остального.
|
||||
bool isSafeCatalogId(const QString &id)
|
||||
{
|
||||
static const QRegularExpression re(QStringLiteral("^[a-z0-9][a-z0-9._-]{0,63}$"));
|
||||
return re.match(id).hasMatch();
|
||||
}
|
||||
|
||||
// Тот же запрет, что у идентификаторов версий в minecraftversion.cpp: строка
|
||||
// становится именем папки в versions.
|
||||
bool isSafeVersionId(const QString &id)
|
||||
{
|
||||
if (id.isEmpty() || id.size() > 128)
|
||||
@@ -61,7 +52,6 @@ bool isSha256Hex(const QString &value)
|
||||
return true;
|
||||
}
|
||||
|
||||
// null или пустая строка — даты нет; в таблице на её месте прочерк.
|
||||
QDate readDate(const QJsonValue &value)
|
||||
{
|
||||
const QString text = value.toString();
|
||||
@@ -73,7 +63,7 @@ QString dateToJson(const QDate &date)
|
||||
return date.isValid() ? date.toString(Qt::ISODate) : QString();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
}
|
||||
|
||||
SeasonalBuildService::SeasonalBuildService(QObject *parent)
|
||||
: QObject(parent)
|
||||
@@ -95,9 +85,6 @@ void SeasonalBuildService::setBaseUrl(const QUrl &baseUrl)
|
||||
return;
|
||||
|
||||
m_baseUrl = baseUrl;
|
||||
// Ссылки на архивы привязаны к прежнему хосту, а проверка их происхождения
|
||||
// сверяется с базовым адресом — держать такие данные после смены сервера
|
||||
// нельзя.
|
||||
m_builds.clear();
|
||||
m_indexById.clear();
|
||||
m_fetchedAt = QDateTime();
|
||||
@@ -154,28 +141,21 @@ QList<SeasonalBuildEntry> SeasonalBuildService::parseCatalog(const QJsonObject &
|
||||
entry.archiveSize = qint64(archive.value(QStringLiteral("size")).toDouble());
|
||||
entry.archiveSha256 = archive.value(QStringLiteral("sha256")).toString().toLower();
|
||||
|
||||
// Данные пришли из сети и превратятся в пути на диске и в запуск игры,
|
||||
// поэтому строка отбрасывается целиком при любой странности.
|
||||
if (!isSafeCatalogId(entry.id) || entry.name.isEmpty() || entry.revision <= 0)
|
||||
continue;
|
||||
if (!isSafeVersionId(entry.minecraftVersion))
|
||||
continue;
|
||||
if (!entry.loader.isEmpty() && !loaderFromKey(entry.loader))
|
||||
continue;
|
||||
// Загрузчик без версии установить нечем.
|
||||
if (!entry.loader.isEmpty() && entry.loaderVersion.isEmpty())
|
||||
continue;
|
||||
if (!isSha256Hex(entry.archiveSha256) || entry.archiveSize < 0)
|
||||
continue;
|
||||
// Ссылка обязана вести на тот же сервер, с которого пришёл каталог:
|
||||
// подменённый index.json иначе увёл бы загрузку куда угодно.
|
||||
if (entry.archiveUrl.scheme() != m_baseUrl.scheme()
|
||||
|| entry.archiveUrl.host() != m_baseUrl.host()
|
||||
|| entry.archiveUrl.port(-1) != m_baseUrl.port(-1)) {
|
||||
continue;
|
||||
}
|
||||
// http допустим только если сам адрес сервера задан по http — так
|
||||
// работает отладка против локального python3 -m http.server.
|
||||
if (entry.archiveUrl.scheme() != QLatin1String("https")
|
||||
&& entry.archiveUrl.scheme() != QLatin1String("http")) {
|
||||
continue;
|
||||
@@ -184,7 +164,6 @@ QList<SeasonalBuildEntry> SeasonalBuildService::parseCatalog(const QJsonObject &
|
||||
list << entry;
|
||||
}
|
||||
|
||||
// Свежие сезоны сверху: сначала те, что ещё идут, потом по дате старта.
|
||||
std::stable_sort(list.begin(), list.end(),
|
||||
[](const SeasonalBuildEntry &a, const SeasonalBuildEntry &b) {
|
||||
const bool aOpen = !a.seasonEnd.isValid();
|
||||
@@ -220,8 +199,6 @@ void SeasonalBuildService::ensureLoaded(Callback callback, bool forceRefresh)
|
||||
const bool fresh = hasData() && m_fetchedAt.isValid()
|
||||
&& m_fetchedAt.secsTo(QDateTime::currentDateTimeUtc()) < kCacheTtlSeconds;
|
||||
if (!forceRefresh && fresh) {
|
||||
// Колбэк, который то синхронный, то нет, ломает вызывающий код —
|
||||
// на попадании в кэш отвечаем через событийный цикл.
|
||||
if (callback)
|
||||
QTimer::singleShot(0, this, [callback = std::move(callback)] { callback(true, {}); });
|
||||
return;
|
||||
@@ -260,10 +237,6 @@ void SeasonalBuildService::startRefresh()
|
||||
QString parseError;
|
||||
QList<SeasonalBuildEntry> parsed =
|
||||
parseCatalog(QJsonDocument::fromJson(reply->readAll()).object(), &parseError);
|
||||
// Пустой разбор — испорченный ответ; хороший кэш им не затираем.
|
||||
// Пустой каталог на сервере от испорченного не отличить, поэтому
|
||||
// «сборок нет» и «ответ битый» — одно и то же состояние: список
|
||||
// остаётся прежним, а причина уходит в lastError.
|
||||
if (parsed.isEmpty()) {
|
||||
finishRefresh(false,
|
||||
parseError.isEmpty() ? Loc::text("seasonal.error.catalogEmpty")
|
||||
@@ -284,14 +257,10 @@ void SeasonalBuildService::finishRefresh(bool ok, const QString &warning)
|
||||
m_lastError = ok ? QString() : warning;
|
||||
m_refreshing = false;
|
||||
emit refreshingChanged();
|
||||
// Строка ошибки — часть того, что показывает окно, поэтому список
|
||||
// перечитывается и на неудаче.
|
||||
emit buildsChanged();
|
||||
|
||||
// Колбэк может снова позвать ensureLoaded — список забираем себе заранее.
|
||||
const QList<Callback> pending = std::exchange(m_pending, {});
|
||||
for (const Callback &callback : pending) {
|
||||
// Сеть недоступна, но старые данные есть — работаем на них.
|
||||
if (!ok && hasData())
|
||||
callback(true, warning);
|
||||
else
|
||||
@@ -306,7 +275,6 @@ bool SeasonalBuildService::readCache()
|
||||
return false;
|
||||
|
||||
const QJsonObject root = QJsonDocument::fromJson(file.readAll()).object();
|
||||
// Кэш от другого сервера бесполезен: ссылки в нём ведут не туда.
|
||||
if (root.value(QStringLiteral("baseUrl")).toString() != m_baseUrl.toString())
|
||||
return false;
|
||||
|
||||
@@ -322,7 +290,6 @@ bool SeasonalBuildService::readCache()
|
||||
|
||||
void SeasonalBuildService::writeCache()
|
||||
{
|
||||
// Папку кэша могли удалить уже после запуска лаунчера.
|
||||
if (!LauncherPaths::ensureRootExists())
|
||||
return;
|
||||
|
||||
@@ -347,8 +314,6 @@ void SeasonalBuildService::writeCache()
|
||||
{QStringLiteral("sha256"), entry.archiveSha256}}}};
|
||||
}
|
||||
|
||||
// Своя обёртка: в каталоге нет ни отметки свежести кэша, ни адреса, с
|
||||
// которого он приехал, — а без них кэш нечем проверить.
|
||||
const QJsonObject root{{QStringLiteral("formatVersion"), kSupportedFormatVersion},
|
||||
{QStringLiteral("fetchedAt"), m_fetchedAt.toString(Qt::ISODate)},
|
||||
{QStringLiteral("baseUrl"), m_baseUrl.toString()},
|
||||
|
||||
Reference in New Issue
Block a user