Some checks failed
CodeQL Code Scanning / CodeQL (push) Has been cancelled
Build Application / Build Debug (push) Has been cancelled
Nix / Build (${{ matrix.system }}) (macos-13, x86_64-darwin) (push) Has been cancelled
Nix / Build (${{ matrix.system }}) (macos-14, aarch64-darwin) (push) Has been cancelled
Nix / Build (${{ matrix.system }}) (ubuntu-22.04, x86_64-linux) (push) Has been cancelled
Nix / Build (${{ matrix.system }}) (ubuntu-22.04-arm, aarch64-linux) (push) Has been cancelled
Update Flake Lockfile / update-flake (push) Has been cancelled
260 lines
9.3 KiB
C++
Executable File
260 lines
9.3 KiB
C++
Executable File
// 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 <quagzipfile.h>
|
||
#include <QByteArray>
|
||
#include <QFileInfo>
|
||
#include <QIODevice>
|
||
#include <QString>
|
||
#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 */
|
||
// };
|
||
|
||
bool readLonglink(QIODevice* in, qint64 size, QByteArray& longlink)
|
||
{
|
||
qint64 n = 0;
|
||
size--; // ignore trailing null
|
||
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;
|
||
}
|
||
|
||
int getOctal(char* buffer, int maxlenght, bool* ok)
|
||
{
|
||
return QByteArray(buffer, qstrnlen(buffer, maxlenght)).toInt(ok, 8);
|
||
}
|
||
|
||
QString decodeName(char* name)
|
||
{
|
||
return QFile::decodeName(QByteArray(name, qstrnlen(name, 100)));
|
||
}
|
||
bool Tar::extract(QIODevice* in, QString dst)
|
||
{
|
||
char buffer[BLOCKSIZE];
|
||
QString name, symlink, firstFolderName;
|
||
bool doNotReset = false, ok;
|
||
while (true) {
|
||
auto n = in->read(buffer, BLOCKSIZE);
|
||
if (n != BLOCKSIZE) { // allways expect complete blocks
|
||
qCritical() << "The expected blocksize was not respected";
|
||
return false;
|
||
}
|
||
if (buffer[0] == 0) { // end of archive
|
||
return true;
|
||
}
|
||
int mode = getOctal(buffer + 100, 8, &ok) | QFile::ReadUser | QFile::WriteUser; // hack to ensure write and read permisions
|
||
if (!ok) {
|
||
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 = decodeName(buffer);
|
||
if (!firstFolderName.isEmpty() && name.startsWith(firstFolderName)) {
|
||
name = name.mid(firstFolderName.size());
|
||
}
|
||
}
|
||
if (symlink.isEmpty())
|
||
symlink = decodeName(buffer);
|
||
qint64 size = getOctal(buffer + 124, 12, &ok);
|
||
if (!ok) {
|
||
qCritical() << "The file size can't be read";
|
||
return false;
|
||
}
|
||
switch (TypeFlag(buffer[156])) {
|
||
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)) {
|
||
qCritical() << "Can't open file:" << fileName;
|
||
return false;
|
||
}
|
||
out.setPermissions(QFile::Permissions(mode));
|
||
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(qint64(BLOCKSIZE), size));
|
||
out.write(tmp);
|
||
size -= BLOCKSIZE;
|
||
}
|
||
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, size, 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, size, 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);
|
||
} |