Files
PrismLauncher/launcher/archive/ArchiveReader.cpp
Trial97 441fb4a891 load the path name with local8Bit if not utf
extended to the symlinks stuff so I geneeralized the function:
- if I can get the utf8(the best outcome) I will use fromUtf8
- if not I will fall back to normal funciton and decode it with
fromLocal8Bit

This convention applies to:
- archive_entry_pathname
- archive_entry_symlink
- archive_entry_hardlink

Signed-off-by: Trial97 <alexandru.tripon97@gmail.com>
2026-03-17 09:12:27 +02:00

298 lines
9.2 KiB
C++

// SPDX-License-Identifier: GPL-3.0-only AND LicenseRef-PublicDomain
/*
* Prism Launcher - Minecraft Launcher
* Copyright (c) 2025 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/>.
*
* Additional note: Portions of this file are released into the public domain
* under LicenseRef-PublicDomain.
*/
#include "ArchiveReader.h"
#include <archive.h>
#include <archive_entry.h>
#include <QDir>
#include <QFileInfo>
#include <QUrl>
#include <functional>
#include <memory>
#include <optional>
namespace MMCZip {
QStringList ArchiveReader::getFiles()
{
return m_fileNames;
}
bool ArchiveReader::collectFiles(bool onlyFiles)
{
return parse([this, onlyFiles](File* f) {
if (!onlyFiles || f->isFile()) {
m_fileNames << f->filename();
}
return f->skip();
});
}
using getPathFunc = std::function<const char*(archive_entry*)>;
static QString decodeLibArchivePath(archive_entry* entry, const getPathFunc& getUtf8Path, const getPathFunc& getPath)
{
auto fileName = QString::fromUtf8(getUtf8Path(entry));
if (fileName.isEmpty()) {
fileName = QString::fromLocal8Bit(getPath(entry));
}
return fileName;
}
QString ArchiveReader::File::filename()
{
return decodeLibArchivePath(m_entry, archive_entry_pathname_utf8, archive_entry_pathname);
}
QByteArray ArchiveReader::File::readAll(int* outStatus)
{
QByteArray data;
const void* buff = nullptr;
size_t size = 0;
la_int64_t offset = 0;
int status = 0;
while ((status = archive_read_data_block(m_archive.get(), &buff, &size, &offset)) == ARCHIVE_OK) {
data.append(static_cast<const char*>(buff), static_cast<qsizetype>(size));
}
if (status != ARCHIVE_EOF && status != ARCHIVE_OK) {
qWarning() << "libarchive read error:" << archive_error_string(m_archive.get());
}
if (outStatus) {
*outStatus = status;
}
return data;
}
QDateTime ArchiveReader::File::dateTime()
{
auto mtime = archive_entry_mtime(m_entry);
auto mtime_nsec = archive_entry_mtime_nsec(m_entry);
auto dt = QDateTime::fromSecsSinceEpoch(mtime);
return dt.addMSecs(mtime_nsec / 1e6);
}
int ArchiveReader::File::readNextHeader()
{
return archive_read_next_header(m_archive.get(), &m_entry);
}
auto ArchiveReader::goToFile(const QString& filename) -> std::unique_ptr<File>
{
auto f = std::make_unique<File>();
auto* a = f->m_archive.get();
archive_read_support_format_all(a);
archive_read_support_filter_all(a);
auto fileName = m_archivePath.toStdWString();
if (archive_read_open_filename_w(a, fileName.data(), m_blockSize) != ARCHIVE_OK) {
qCritical() << "Failed to open archive file:" << m_archivePath << "-" << archive_error_string(a);
return nullptr;
}
while (f->readNextHeader() == ARCHIVE_OK) {
if (f->filename() == filename) {
return f;
}
f->skip();
}
archive_read_close(a);
return nullptr;
}
static int copy_data(struct archive* ar, struct archive* aw, bool notBlock = false)
{
int r = 0;
const void* buff = nullptr;
size_t size = 0;
la_int64_t offset = 0;
for (;;) {
r = archive_read_data_block(ar, &buff, &size, &offset);
if (r == ARCHIVE_EOF) {
return ARCHIVE_OK;
}
if (r < ARCHIVE_OK) {
qCritical() << "Failed reading data block:" << archive_error_string(ar);
return (r);
}
if (notBlock) {
r = archive_write_data(aw, buff, size);
} else {
r = archive_write_data_block(aw, buff, size, offset);
}
if (r < ARCHIVE_OK) {
qCritical() << "Failed writing data block:" << archive_error_string(aw);
return (r);
}
}
}
static bool willEscapeRoot(const QDir& root, archive_entry* entry)
{
auto entryPath = decodeLibArchivePath(entry, archive_entry_pathname_utf8, archive_entry_pathname);
auto linkTarget = decodeLibArchivePath(entry, archive_entry_symlink_utf8, archive_entry_symlink);
auto hardLink = decodeLibArchivePath(entry, archive_entry_hardlink_utf8, archive_entry_hardlink);
if (entryPath.isEmpty() || (linkTarget.isEmpty() && hardLink.isEmpty())) {
return false;
}
bool isHardLink = false;
if (isHardLink = linkTarget.isEmpty(); isHardLink) {
linkTarget = hardLink;
}
QString linkFullPath = root.filePath(entryPath);
auto rootDir = QUrl::fromLocalFile(root.absolutePath());
if (!rootDir.isParentOf(QUrl::fromLocalFile(linkFullPath))) {
return true;
}
QDir linkDir = QFileInfo(linkFullPath).dir();
if (!QDir::isAbsolutePath(linkTarget)) {
linkTarget = (!isHardLink ? linkDir : root).filePath(linkTarget);
}
return !rootDir.isParentOf(QUrl::fromLocalFile(QDir::cleanPath(linkTarget)));
}
bool ArchiveReader::File::writeFile(archive* out, const QString& targetFileName, bool notBlock)
{
return writeFile(out, targetFileName, {}, notBlock);
};
bool ArchiveReader::File::writeFile(archive* out, const QString& targetFileName, std::optional<QDir> root, bool notBlock)
{
auto* entry = m_entry;
std::unique_ptr<archive_entry, decltype(&archive_entry_free)> entryClone(nullptr, &archive_entry_free);
if (!targetFileName.isEmpty()) {
entryClone.reset(archive_entry_clone(m_entry));
entry = entryClone.get();
auto nameUtf8 = targetFileName.toUtf8();
archive_entry_set_pathname_utf8(entry, nameUtf8.constData());
}
if (root.has_value() && willEscapeRoot(root.value(), entry)) {
qCritical() << "Failed to write header to entry:" << filename() << "-" << "file outside root";
return false;
}
if (archive_write_header(out, entry) < ARCHIVE_OK) {
qCritical() << "Failed to write header to entry:" << filename() << "-" << archive_error_string(out) << targetFileName;
return false;
}
if (archive_entry_size(m_entry) > 0) {
auto r = copy_data(m_archive.get(), out, notBlock);
if (r < ARCHIVE_OK) {
qCritical() << "Failed reading data block:" << archive_error_string(out);
}
if (r < ARCHIVE_WARN) {
return false;
}
}
auto r = archive_write_finish_entry(out);
if (r < ARCHIVE_OK) {
qCritical() << "Failed to finish writing entry:" << archive_error_string(out);
}
return (r >= ARCHIVE_WARN);
}
bool ArchiveReader::parse(const std::function<bool(File*, bool&)>& doStuff)
{
auto f = std::make_unique<File>();
auto* a = f->m_archive.get();
archive_read_support_format_all(a);
archive_read_support_filter_all(a);
auto fileName = m_archivePath.toStdWString();
if (archive_read_open_filename_w(a, fileName.data(), m_blockSize) != ARCHIVE_OK) {
qCritical() << "Failed to open archive file:" << m_archivePath << "-" << f->error();
return false;
}
bool breakControl = false;
while (f->readNextHeader() == ARCHIVE_OK) {
if (f && !doStuff(f.get(), breakControl)) {
qCritical() << "Failed to parse file:" << f->filename() << "-" << f->error();
return false;
}
if (breakControl) {
break;
}
}
archive_read_close(a);
return true;
}
bool ArchiveReader::parse(const std::function<bool(File*)>& doStuff)
{
return parse([doStuff](File* f, bool&) { return doStuff(f); });
}
bool ArchiveReader::File::isFile()
{
return (archive_entry_filetype(m_entry) & AE_IFMT) == AE_IFREG;
}
bool ArchiveReader::File::skip()
{
return archive_read_data_skip(m_archive.get()) == ARCHIVE_OK;
}
const char* ArchiveReader::File::error()
{
return archive_error_string(m_archive.get());
}
QString ArchiveReader::getZipName()
{
return m_archivePath;
}
bool ArchiveReader::exists(const QString& filePath) const
{
if (filePath == QLatin1String("/") || filePath.isEmpty()) {
return true;
}
// Normalize input path (remove trailing slash, if any)
QString normalizedPath = QDir::cleanPath(filePath);
if (normalizedPath.startsWith('/')) {
normalizedPath.remove(0, 1);
}
if (normalizedPath == QLatin1String(".")) {
return true;
}
if (normalizedPath == QLatin1String("..")) {
return false; // root only
}
// Check for exact file match
if (m_fileNames.contains(normalizedPath, Qt::CaseInsensitive)) {
return true;
}
// Check for directory existence by seeing if any file starts with that path
QString dirPath = normalizedPath + QLatin1Char('/');
for (const QString& f : m_fileNames) {
if (f.startsWith(dirPath, Qt::CaseInsensitive)) {
return true;
}
}
return false;
}
ArchiveReader::File::File() : m_archive(ArchivePtr(archive_read_new(), archive_read_free)) {}
} // namespace MMCZip