Made tar.gz parser

Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
This commit is contained in:
Trial97 2024-03-21 19:46:57 +02:00
parent ab7fc2e46c
commit 1a6dfd04d6
No known key found for this signature in database
GPG Key ID: 55EF5DA53DB36318
15 changed files with 381 additions and 6 deletions

View File

@ -869,6 +869,7 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
m_metacache->addBase("translations", QDir("translations").absolutePath());
m_metacache->addBase("icons", QDir("cache/icons").absolutePath());
m_metacache->addBase("meta", QDir("meta").absolutePath());
m_metacache->addBase("java", QDir("cache/java").absolutePath());
m_metacache->Load();
qDebug() << "<> Cache initialized.";
}

View File

@ -78,6 +78,14 @@ QVariant BaseVersionList::data(const QModelIndex& index, int role) const
case TypeRole:
return version->typeString();
case JavaMajorRole: {
auto major = version->name();
if (major.startsWith("java")) {
major = "Java " + major.mid(4);
}
return major;
}
default:
return QVariant();
}
@ -112,5 +120,6 @@ QHash<int, QByteArray> BaseVersionList::roleNames() const
roles.insert(PathRole, "path");
roles.insert(JavaNameRole, "javaName");
roles.insert(CPUArchitectureRole, "architecture");
roles.insert(JavaMajorRole, "javaMajor");
return roles;
}

View File

@ -49,6 +49,7 @@ class BaseVersionList : public QAbstractListModel {
BranchRole,
PathRole,
JavaNameRole,
JavaMajorRole,
CPUArchitectureRole,
SortRole
};

View File

@ -24,6 +24,8 @@ set(CORE_SOURCES
NullInstance.h
MMCZip.h
MMCZip.cpp
Untar.h
Untar.cpp
StringUtils.h
StringUtils.cpp
QVariantUtils.h

View File

@ -276,6 +276,9 @@ bool ensureFolderPathExists(const QFileInfo folderPath)
{
QDir dir;
QString ensuredPath = folderPath.filePath();
if (folderPath.exists())
return true;
bool success = dir.mkpath(ensuredPath);
return success;
}

262
launcher/Untar.cpp Normal file
View File

@ -0,0 +1,262 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023-2024 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "Untar.h"
#include <qfileinfo.h>
#include <qlogging.h>
#include <quagzipfile.h>
#include <QByteArray>
#include <QIODevice>
#include <QString>
#include <cstdlib>
#include "FileSystem.h"
// adaptation of the:
// - https://github.com/madler/zlib/blob/develop/contrib/untgz/untgz.c
// - https://en.wikipedia.org/wiki/Tar_(computing)
// - https://github.com/euroelessar/cutereader/blob/master/karchive/src/ktar.cpp
#define BLOCKSIZE 512
#define SHORTNAMESIZE 100
enum class TypeFlag : char {
Regular = '0', // regular file
ARegular = 0, // regular file
Link = '1', // link
Symlink = '2', // reserved
Character = '3', // character special
Block = '4', // block special
Directory = '5', // directory
FIFO = '6', // FIFO special
Contiguous = '7', // reserved
// Posix stuff
GlobalPosixHeader = 'g',
ExtendedPosixHeader = 'x',
// 'A' 'Z' Vendor specific extensions(POSIX .1 - 1988)
// GNU
GNULongLink = 'K', /* long link name */
GNULongName = 'L', /* long file name */
};
struct Header { /* byte offset */
char name[100]; /* 0 */
char mode[8]; /* 100 */
char uid[8]; /* 108 */
char gid[8]; /* 116 */
char size[12]; /* 124 */
char mtime[12]; /* 136 */
char chksum[8]; /* 148 */
TypeFlag typeflag; /* 156 */
char linkname[100]; /* 157 */
char magic[6]; /* 257 */
char version[2]; /* 263 */
char uname[32]; /* 265 */
char gname[32]; /* 297 */
char devmajor[8]; /* 329 */
char devminor[8]; /* 337 */
char prefix[155]; /* 345 */
/* 500 */
};
union Buffer {
char buffer[BLOCKSIZE];
struct Header header;
};
bool readLonglink(QIODevice* in, Buffer buffer, QByteArray& longlink)
{
qint64 n = 0;
qint64 size = strtoll(buffer.header.size, NULL, 8);
size--; // ignore trailing null
if (errno == ERANGE) {
qCritical() << "The filename size can't be read";
return false;
}
if (size < 0) {
qCritical() << "The filename size is negative";
return false;
}
longlink.resize(size + (BLOCKSIZE - size % BLOCKSIZE)); // make the size divisible by BLOCKSIZE
for (qint64 offset = 0; offset < longlink.size(); offset += BLOCKSIZE) {
n = in->read(longlink.data() + offset, BLOCKSIZE);
if (n != BLOCKSIZE) {
qCritical() << "The expected blocksize was not respected for the name";
return false;
}
}
longlink.truncate(qstrlen(longlink.constData()));
return true;
}
bool Tar::extract(QIODevice* in, QString dst)
{
Buffer buffer;
QString name, symlink, firstFolderName;
bool doNotReset = false;
while (true) {
auto n = in->read(buffer.buffer, BLOCKSIZE);
if (n != BLOCKSIZE) { // allways expect complete blocks
qCritical() << "The expected blocksize was not respected";
return false;
}
if (buffer.header.name[0] == 0) { // end of archive
return true;
}
int mode = strtol(buffer.header.mode, NULL, 8) | QFile::ReadUser | QFile::WriteUser; // hack to ensure write and read permisions
if (errno == ERANGE) {
qCritical() << "The file mode can't be read";
return false;
}
// there are names that are exactly 100 bytes long
// and neither longlink nor \0 terminated (bug:101472)
if (name.isEmpty()) {
name = QFile::decodeName(QByteArray(buffer.header.name, qstrnlen(buffer.header.name, 100)));
if (!firstFolderName.isEmpty() && name.startsWith(firstFolderName)) {
name = name.mid(firstFolderName.size());
}
}
if (symlink.isEmpty())
symlink = QFile::decodeName(QByteArray(buffer.header.linkname, qstrnlen(buffer.header.linkname, 100)));
switch (buffer.header.typeflag) {
case TypeFlag::Regular:
/* fallthrough */
case TypeFlag::ARegular: {
auto fileName = FS::PathCombine(dst, name);
if (!FS::ensureFilePathExists(fileName)) {
qCritical() << "Can't ensure the file path to exist: " << fileName;
return false;
}
QFile out(fileName);
if (!out.open(QFile::WriteOnly, QFile::Permission(mode))) {
qCritical() << "Can't open file:" << fileName;
return false;
}
qint64 size = strtoll(buffer.header.size, NULL, 8);
if (errno == ERANGE) {
qCritical() << "The file size can't be read";
return false;
}
while (size > 0) {
QByteArray tmp(BLOCKSIZE, 0);
n = in->read(tmp.data(), BLOCKSIZE);
if (n != BLOCKSIZE) {
qCritical() << "The expected blocksize was not respected when reading file";
return false;
}
tmp.truncate(qMin(BLOCKSIZE, size));
out.write(tmp);
size -= BLOCKSIZE;
}
QFile::setPermissions(fileName, QFile::Permissions(mode));
break;
}
case TypeFlag::Directory: {
if (firstFolderName.isEmpty()) {
firstFolderName = name;
break;
}
auto folderPath = FS::PathCombine(dst, name);
if (!FS::ensureFolderPathExists(folderPath)) {
qCritical() << "Can't ensure that folder exists: " << folderPath;
return false;
}
break;
}
case TypeFlag::GNULongLink: {
doNotReset = true;
QByteArray longlink;
if (readLonglink(in, buffer, longlink)) {
symlink = QFile::decodeName(longlink.constData());
} else {
qCritical() << "Failed to read long link";
return false;
}
break;
}
case TypeFlag::GNULongName: {
doNotReset = true;
QByteArray longlink;
if (readLonglink(in, buffer, longlink)) {
name = QFile::decodeName(longlink.constData());
} else {
qCritical() << "Failed to read long name";
return false;
}
break;
}
case TypeFlag::Link:
/* fallthrough */
case TypeFlag::Symlink: {
auto fileName = FS::PathCombine(dst, name);
if (!FS::create_link(FS::PathCombine(QFileInfo(fileName).path(), symlink), fileName)()) { // do not use symlinks
qCritical() << "Can't create link for:" << fileName << " to:" << FS::PathCombine(QFileInfo(fileName).path(), symlink);
return false;
}
FS::ensureFilePathExists(fileName);
QFile::setPermissions(fileName, QFile::Permissions(mode));
break;
}
case TypeFlag::Character:
/* fallthrough */
case TypeFlag::Block:
/* fallthrough */
case TypeFlag::FIFO:
/* fallthrough */
case TypeFlag::Contiguous:
/* fallthrough */
case TypeFlag::GlobalPosixHeader:
/* fallthrough */
case TypeFlag::ExtendedPosixHeader:
/* fallthrough */
default:
break;
}
if (!doNotReset) {
name.truncate(0);
symlink.truncate(0);
}
doNotReset = false;
}
return true;
}
bool GZTar::extract(QString src, QString dst)
{
QuaGzipFile a(src);
if (!a.open(QIODevice::ReadOnly)) {
qCritical() << "Can't open tar file:" << src;
return false;
}
return Tar::extract(&a, dst);
}

46
launcher/Untar.h Normal file
View File

@ -0,0 +1,46 @@
// SPDX-License-Identifier: GPL-3.0-only
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2023-2024 Trial97 <alexandru.tripon97@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <QIODevice>
// this is a hack used for the java downloader (feel free to remove it in favor of a library)
// both extract functions will extract the first folder inside dest(disregarding the prefix)
namespace Tar {
bool extract(QIODevice* in, QString dst);
};
namespace GZTar {
bool extract(QString src, QString dst);
};

View File

@ -120,6 +120,8 @@ QVariant VersionProxyModel::headerData(int section, Qt::Orientation orientation,
return tr("Path");
case JavaName:
return tr("Java Name");
case JavaMajor:
return tr("Major");
case Time:
return tr("Released");
}
@ -139,6 +141,8 @@ QVariant VersionProxyModel::headerData(int section, Qt::Orientation orientation,
return tr("Filesystem path to this version");
case JavaName:
return tr("The alternative name of the java version");
case JavaMajor:
return tr("The java major version");
case Time:
return tr("Release date of this version");
}
@ -175,6 +179,8 @@ QVariant VersionProxyModel::data(const QModelIndex& index, int role) const
return sourceModel()->data(parentIndex, BaseVersionList::PathRole);
case JavaName:
return sourceModel()->data(parentIndex, BaseVersionList::JavaNameRole);
case JavaMajor:
return sourceModel()->data(parentIndex, BaseVersionList::JavaMajorRole);
case Time:
return sourceModel()->data(parentIndex, Meta::VersionList::TimeRole).toDate();
default:
@ -323,6 +329,9 @@ void VersionProxyModel::setSourceModel(QAbstractItemModel* replacingRaw)
if (roles.contains(BaseVersionList::JavaNameRole)) {
m_columns.push_back(JavaName);
}
if (roles.contains(BaseVersionList::JavaMajorRole)) {
m_columns.push_back(JavaMajor);
}
if (roles.contains(Meta::VersionList::TimeRole)) {
m_columns.push_back(Time);
}

View File

@ -9,7 +9,7 @@ class VersionFilterModel;
class VersionProxyModel : public QAbstractProxyModel {
Q_OBJECT
public:
enum Column { Name, ParentVersion, Branch, Type, CPUArchitecture, Path, Time, JavaName };
enum Column { Name, ParentVersion, Branch, Type, CPUArchitecture, Path, Time, JavaName, JavaMajor };
using FilterMap = QHash<BaseVersionList::ModelRoles, std::shared_ptr<Filter>>;
public:

View File

@ -21,6 +21,7 @@
#include "MMCZip.h"
#include "Application.h"
#include "Untar.h"
#include "net/ChecksumValidator.h"
#include "net/NetJob.h"
#include "tasks/Task.h"
@ -69,6 +70,28 @@ void ArchiveDownloadTask::executeTask()
void ArchiveDownloadTask::extractJava(QString input)
{
setStatus(tr("Extracting java"));
if (input.endsWith("tar")) {
setStatus(tr("Extracting java(the progress will not be reported for tar)"));
QFile in(input);
if (!in.open(QFile::ReadOnly)) {
emitFailed(tr("Unable to open supplied tar file."));
return;
}
if (!Tar::extract(&in, QDir(m_final_path).absolutePath())) {
emitFailed(tr("Unable to extract supplied tar file."));
return;
}
emitSucceeded();
return;
} else if (input.endsWith("tar.gz") || input.endsWith("taz") || input.endsWith("tgz")) {
setStatus(tr("Extracting java(the progress will not be reported for tar)"));
if (!GZTar::extract(input, QDir(m_final_path).absolutePath())) {
emitFailed(tr("Unable to extract supplied tar file."));
return;
}
emitSucceeded();
return;
}
auto zip = std::make_shared<QuaZip>(input);
if (!zip->open(QuaZip::mdUnzip)) {
emitFailed(tr("Unable to open supplied zip file."));

View File

@ -92,6 +92,13 @@ QVariant VersionList::data(const QModelIndex& index, int role) const
return QVariant::fromValue(version);
case RecommendedRole:
return version->isRecommended();
case JavaMajorRole: {
auto major = version->version();
if (major.startsWith("java")) {
major = "Java " + major.mid(4);
}
return major;
}
// FIXME: this should be determined in whatever view/proxy is used...
// case LatestRole: return version == getLatestStable();
default:

View File

@ -54,7 +54,6 @@ void VerifyJavaInstall::executeTask()
auto javaArchitecture = settings->get("JavaArchitecture").toString();
auto maxMemAlloc = settings->get("MaxMemAlloc").toInt();
emit logLine(tr("Java architecture is x%1.").arg(javaArchitecture), MessageLevel::Info);
if (javaArchitecture == "32" && maxMemAlloc > 2048) {
emit logLine(tr("Max memory allocation exceeds the supported value.\n"
"The selected java is 32-bit and doesn't support more than 2048MiB of RAM.\n"

View File

@ -19,16 +19,19 @@
#include "InstallJavaDialog.h"
#include <QDialogButtonBox>
#include <QMessageBox>
#include <QPushButton>
#include <QVBoxLayout>
#include <QWidget>
#include "Application.h"
#include "BaseVersionList.h"
#include "FileSystem.h"
#include "java/download/ArchiveDownloadTask.h"
#include "java/download/ManifestDownloadTask.h"
#include "meta/Index.h"
#include "meta/VersionList.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/ProgressDialog.h"
#include "ui/java/VersionList.h"
#include "ui/widgets/PageContainer.h"
@ -71,8 +74,7 @@ class InstallJavaPage : public QWidget, public BasePage {
//! loads the list if needed.
void initialize(Meta::VersionList::Ptr vlist)
{
vlist->setProvidedRoles({ BaseVersionList::VersionRole, BaseVersionList::RecommendedRole, BaseVersionList::VersionPointerRole });
vlist->sort(1);
vlist->setProvidedRoles({ BaseVersionList::JavaMajorRole, BaseVersionList::RecommendedRole, BaseVersionList::VersionPointerRole });
majorVersionSelect->initialize(vlist.get());
}
@ -219,7 +221,11 @@ void InstallDialog::done(int result)
break;
}
auto deletePath = [final_path] { FS::deletePath(final_path); };
connect(task.get(), &Task::failed, this, deletePath);
connect(task.get(), &Task::failed, this, [this, &deletePath](QString reason) {
QString error = QString("Java download failed: %1").arg(reason);
CustomMessageBox::selectable(this, tr("Error"), error, QMessageBox::Warning)->show();
deletePath();
});
connect(task.get(), &Task::aborted, this, deletePath);
ProgressDialog pg(this);
pg.setSkipButton(true, tr("Abort"));

View File

@ -78,6 +78,13 @@ QVariant VersionList::data(const QModelIndex& index, int role) const
return false; // do not recommend any version
case JavaNameRole:
return version->name();
case JavaMajorRole: {
auto major = version->version.toString();
if (major.startsWith("java")) {
major = "Java " + major.mid(4);
}
return major;
}
case TypeRole:
return version->packageType;
case Meta::VersionList::TimeRole:

View File

@ -180,7 +180,7 @@ void JavaSettingsWidget::initialize()
tr("%1 can automatically download the correct Java version for each version of Minecraft..\n"
"Do you want to enable Java auto-download?\n")
.arg(BuildConfig.LAUNCHER_DISPLAYNAME),
QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes)
QMessageBox::Question, QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes)
->exec();
if (button == QMessageBox::Yes) {
m_autodetectJavaCheckBox->setChecked(true);