mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-28 13:56:59 +01:00
Zarchive Loading (#724)
* Simplify archive loading and harden filesystem integration * Restore directory entry name length assertion * Keep graphics-dependent filesystem tests out of CI --------- Co-authored-by: nmzik <Nmzik@mail.ru>
This commit is contained in:
@@ -20,7 +20,7 @@ add_executable(launcher WIN32
|
||||
)
|
||||
|
||||
|
||||
target_link_libraries(launcher Qt6::Concurrent Qt6::Network Qt6::Widgets Vulkan::Headers SDL3::SDL3-static kyty_git_version)
|
||||
target_link_libraries(launcher Qt6::Concurrent Qt6::Network Qt6::Widgets Vulkan::Headers SDL3::SDL3-static kyty_git_version common)
|
||||
if(WIN32)
|
||||
target_link_libraries(launcher setupapi)
|
||||
endif()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef CONFIGURATION_ITEM_H
|
||||
#define CONFIGURATION_ITEM_H
|
||||
|
||||
#include <QIcon>
|
||||
#include <QObject>
|
||||
#include <QTreeWidgetItem>
|
||||
|
||||
@@ -18,7 +19,7 @@ public:
|
||||
explicit ConfigurationItem(std::unique_ptr<Configuration> info, QTreeWidget* parent);
|
||||
~ConfigurationItem() override;
|
||||
|
||||
void Update();
|
||||
void Update(bool reload_icon = false);
|
||||
bool operator<(const QTreeWidgetItem& other) const override;
|
||||
|
||||
Configuration& GetInfo() { return *m_info; }
|
||||
@@ -35,6 +36,7 @@ private:
|
||||
void UpdateStatusIndicator();
|
||||
|
||||
std::unique_ptr<Configuration> m_info;
|
||||
QIcon m_icon;
|
||||
bool m_running = false;
|
||||
QComboBox* m_status_combo = nullptr;
|
||||
QLabel* m_status_indicator = nullptr;
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef GAME_CONTENT_H
|
||||
#define GAME_CONTENT_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
|
||||
namespace GameContent {
|
||||
|
||||
inline constexpr uint64_t MaxMetadataSize = uint64_t {1} << 20u;
|
||||
inline constexpr uint64_t MaxImageSize = uint64_t {32} << 20u;
|
||||
inline constexpr uint64_t MaxTrophyPackageSize = uint64_t {128} << 20u;
|
||||
|
||||
[[nodiscard]] std::filesystem::path ToPath(const QString& path);
|
||||
[[nodiscard]] QString FromPath(const std::filesystem::path& path);
|
||||
[[nodiscard]] bool IsArchive(const QString& base);
|
||||
[[nodiscard]] std::filesystem::path Resolve(const QString& base, const QString& relative);
|
||||
[[nodiscard]] bool FileExists(const QString& base, const QString& relative);
|
||||
[[nodiscard]] QByteArray ReadPath(const std::filesystem::path& path, uint64_t max_size);
|
||||
[[nodiscard]] QByteArray ReadFile(const QString& base, const QString& relative, uint64_t max_size);
|
||||
[[nodiscard]] QStringList ListFiles(const QString& base, const QString& relative);
|
||||
|
||||
} // namespace GameContent
|
||||
|
||||
#endif // GAME_CONTENT_H
|
||||
@@ -1,6 +1,8 @@
|
||||
#ifndef GAME_LIST_TREE_WIDGET_H
|
||||
#define GAME_LIST_TREE_WIDGET_H
|
||||
|
||||
#include "gameContent.h"
|
||||
|
||||
#include <QPaintEvent>
|
||||
#include <QPainter>
|
||||
#include <QPalette>
|
||||
@@ -17,14 +19,15 @@ public:
|
||||
viewport()->setAutoFillBackground(false);
|
||||
}
|
||||
|
||||
void SetBackgroundImage(const QString& path) {
|
||||
if (m_path == path) {
|
||||
void SetBackgroundImage(const QString& key) {
|
||||
if (m_path == key) {
|
||||
return;
|
||||
}
|
||||
m_path = path;
|
||||
m_path = key;
|
||||
m_source = QPixmap();
|
||||
if (!path.isEmpty()) {
|
||||
m_source.load(path);
|
||||
if (!key.isEmpty()) {
|
||||
m_source.loadFromData(GameContent::ReadFile(key, QStringLiteral("sce_sys/pic0.png"),
|
||||
GameContent::MaxImageSize));
|
||||
}
|
||||
UpdateScaledBackground();
|
||||
viewport()->update();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "configurationItem.h"
|
||||
|
||||
#include "configuration.h"
|
||||
#include "gameContent.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QComboBox>
|
||||
@@ -14,6 +15,7 @@
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QLocale>
|
||||
#include <QPixmap>
|
||||
#include <QSize>
|
||||
#include <QStringList>
|
||||
#include <QStyle>
|
||||
@@ -154,7 +156,7 @@ ConfigurationItem::ConfigurationItem(std::unique_ptr<Configuration> info, QTreeW
|
||||
m_comment_edit->setFrame(false);
|
||||
parent->setItemWidget(this, CommentsColumn, m_comment_edit);
|
||||
|
||||
Update();
|
||||
Update(true);
|
||||
SetRunning(false);
|
||||
|
||||
setData(SizeColumn, Qt::UserRole, qint64(-1));
|
||||
@@ -170,7 +172,14 @@ ConfigurationItem::ConfigurationItem(std::unique_ptr<Configuration> info, QTreeW
|
||||
watcher->deleteLater();
|
||||
});
|
||||
watcher->setFuture(QtConcurrent::run([path = m_info->basedir]() -> qint64 {
|
||||
if (path.isEmpty() || !QDir(path).exists()) {
|
||||
if (path.isEmpty()) {
|
||||
return -1;
|
||||
}
|
||||
const QFileInfo info(path);
|
||||
if (GameContent::IsArchive(path)) {
|
||||
return info.size();
|
||||
}
|
||||
if (!QDir(path).exists()) {
|
||||
return -1;
|
||||
}
|
||||
qint64 bytes = 0;
|
||||
@@ -188,7 +197,7 @@ ConfigurationItem::ConfigurationItem(std::unique_ptr<Configuration> info, QTreeW
|
||||
|
||||
ConfigurationItem::~ConfigurationItem() = default;
|
||||
|
||||
void ConfigurationItem::Update() {
|
||||
void ConfigurationItem::Update(bool reload_icon) {
|
||||
const auto display_text = GetDisplayText(*m_info);
|
||||
const auto path = GetPathText(*m_info);
|
||||
|
||||
@@ -209,6 +218,12 @@ void ConfigurationItem::Update() {
|
||||
m_comment_edit->setText(m_info->game_comment);
|
||||
}
|
||||
|
||||
if (reload_icon) {
|
||||
const auto icon_data = GameContent::ReadFile(
|
||||
m_info->basedir, QStringLiteral("sce_sys/icon0.png"), GameContent::MaxImageSize);
|
||||
QPixmap icon;
|
||||
m_icon = !icon_data.isEmpty() && icon.loadFromData(icon_data) ? QIcon(icon) : QIcon {};
|
||||
}
|
||||
UpdateIcon();
|
||||
UpdateStatusIndicator();
|
||||
}
|
||||
@@ -267,9 +282,8 @@ void ConfigurationItem::SetCompatibilityEditable(bool editable) {
|
||||
}
|
||||
|
||||
void ConfigurationItem::UpdateIcon() {
|
||||
const QString icon_file = QDir(m_info->basedir).filePath(QStringLiteral("sce_sys/icon0.png"));
|
||||
if (QFileInfo::exists(icon_file)) {
|
||||
setIcon(NameColumn, QIcon(icon_file));
|
||||
if (!m_icon.isNull()) {
|
||||
setIcon(NameColumn, m_icon);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#include "configurationListWidget.h"
|
||||
|
||||
#include "common/archive.h"
|
||||
#include "compatibilityDatabase.h"
|
||||
#include "configuration.h"
|
||||
#include "configurationEditDialog.h"
|
||||
#include "configurationItem.h"
|
||||
#include "gameContent.h"
|
||||
#include "gameListTreeWidget.h"
|
||||
#include "inputMappingDialog.h"
|
||||
#include "mainDialog.h"
|
||||
@@ -127,15 +129,6 @@ static QStringList SettingsStringList(const QVariant& value) {
|
||||
return text.isEmpty() ? QStringList() : QStringList({text});
|
||||
}
|
||||
|
||||
static QString GetPic0Path(const Configuration& info) {
|
||||
if (info.basedir.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto path = QDir(info.basedir).filePath(QStringLiteral("sce_sys/pic0.png"));
|
||||
return QFileInfo::exists(path) ? QFileInfo(path).absoluteFilePath() : QString();
|
||||
}
|
||||
|
||||
static void ConfigureGameList(Ui::ConfigurationListWidget* ui) {
|
||||
ui->cfgs_list->setAlternatingRowColors(false);
|
||||
ui->cfgs_list->setAllColumnsShowFocus(true);
|
||||
@@ -490,17 +483,16 @@ static QString GetFirmwareVersion(const QJsonObject& root) {
|
||||
return version;
|
||||
}
|
||||
|
||||
static GameMetadata GetGameMetadata(const QString& param_file, const QString& fallback) {
|
||||
static GameMetadata GetGameMetadata(const QByteArray& param_data, const QString& fallback) {
|
||||
GameMetadata ret;
|
||||
ret.title_name = fallback;
|
||||
|
||||
QFile param(param_file);
|
||||
if (!param.open(QIODevice::ReadOnly)) {
|
||||
if (param_data.isEmpty()) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
QJsonParseError parse_error;
|
||||
const auto doc = QJsonDocument::fromJson(param.readAll(), &parse_error);
|
||||
const auto doc = QJsonDocument::fromJson(param_data, &parse_error);
|
||||
if (parse_error.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
return ret;
|
||||
}
|
||||
@@ -522,18 +514,18 @@ static GameMetadata GetGameMetadata(const QString& param_file, const QString& fa
|
||||
}
|
||||
|
||||
static void SetGameFiles(Configuration& info, const QString& game_dir, const QString& game_path,
|
||||
const GameMetadata& metadata) {
|
||||
QDir game(game_dir);
|
||||
const GameMetadata& metadata, bool archive) {
|
||||
const QFileInfo game(game_dir);
|
||||
|
||||
info.game_path = game_path;
|
||||
info.basedir = game.absolutePath();
|
||||
info.basedir = archive ? game.absoluteFilePath() : QDir(game_dir).absolutePath();
|
||||
info.name = metadata.title_name;
|
||||
info.title_id = metadata.title_id;
|
||||
info.gameVersion = metadata.gameVersion;
|
||||
info.firmwareVer = metadata.firmwareVer;
|
||||
|
||||
if (info.name.isEmpty()) {
|
||||
info.name = game.dirName();
|
||||
info.name = archive ? game.completeBaseName() : QDir(game_dir).dirName();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -609,6 +601,94 @@ void ConfigurationListWidget::ScanGameDirectory() {
|
||||
|
||||
const QString eboot_name = QStringLiteral("eboot.bin");
|
||||
QSet<QString> found_games;
|
||||
const auto add_game = [this, &found_games, &eboot_name,
|
||||
&running_items](const QString& base, const QString& game_path,
|
||||
const QString& legacy_game_path, const QString& fallback,
|
||||
bool archive) {
|
||||
const QString game_key = PathKey(game_path);
|
||||
if (game_key.isEmpty() || found_games.contains(game_key)) {
|
||||
return;
|
||||
}
|
||||
// Keep the index and decompression cache alive through validation, metadata and icon
|
||||
// reads, after rejecting duplicate candidates from overlapping game folders.
|
||||
const auto reader = archive ? Common::OpenArchive(GameContent::ToPath(base)) : nullptr;
|
||||
if (archive && (reader == nullptr || !GameContent::FileExists(base, eboot_name))) {
|
||||
return;
|
||||
}
|
||||
found_games.insert(game_key);
|
||||
|
||||
const auto metadata =
|
||||
GetGameMetadata(GameContent::ReadFile(base, QStringLiteral("sce_sys/param.json"),
|
||||
GameContent::MaxMetadataSize),
|
||||
fallback);
|
||||
auto info = std::make_unique<Configuration>();
|
||||
info->custom_settings =
|
||||
FindCustomInfo(&m_custom_infos, game_path, legacy_game_path) != nullptr;
|
||||
|
||||
SetGameFiles(*info, base, game_path, metadata, archive);
|
||||
const auto* compatibility = m_compatibility->Find(info->title_id);
|
||||
if (compatibility != nullptr) {
|
||||
info->game_status = compatibility->status;
|
||||
info->game_comment = compatibility->comment;
|
||||
}
|
||||
|
||||
if (auto* item = running_items.value(game_key); item != nullptr) {
|
||||
// Refresh metadata without losing the running row's identity.
|
||||
const QSignalBlocker status_blocker(item->GetStatusCombo());
|
||||
item->GetInfo().CopyGameInfoFrom(*info);
|
||||
item->Update(true);
|
||||
item->SetCompatibilityEditable(m_compatibility->IsLocal() &&
|
||||
!item->GetInfo().title_id.trimmed().isEmpty());
|
||||
return;
|
||||
}
|
||||
|
||||
auto* item = new ConfigurationItem(std::move(info), m_ui->cfgs_list);
|
||||
item->SetCompatibilityEditable(m_compatibility->IsLocal() &&
|
||||
!item->GetInfo().title_id.trimmed().isEmpty());
|
||||
connect(item->GetStatusCombo(), &QComboBox::currentIndexChanged, item,
|
||||
[this, item](int /*index*/) {
|
||||
if (!m_compatibility->IsLocal()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& title_id = item->GetInfo().title_id;
|
||||
if (title_id.trimmed().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
item->GetInfo().game_status = static_cast<Configuration::GameStatus>(
|
||||
item->GetStatusCombo()->currentData().toInt());
|
||||
item->Update();
|
||||
m_compatibility->SetStatus(title_id, item->GetInfo().game_status);
|
||||
m_ui->cfgs_list->setCurrentItem(item);
|
||||
SelectItem(item);
|
||||
});
|
||||
connect(item->GetCommentEdit(), &QLineEdit::editingFinished, item, [this, item]() {
|
||||
if (!m_compatibility->IsLocal()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& title_id = item->GetInfo().title_id;
|
||||
if (title_id.trimmed().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
item->GetInfo().game_comment = item->GetCommentEdit()->text();
|
||||
item->Update();
|
||||
m_compatibility->SetComment(title_id, item->GetInfo().game_comment);
|
||||
m_ui->cfgs_list->setCurrentItem(item);
|
||||
SelectItem(item);
|
||||
});
|
||||
};
|
||||
const auto scan_archives = [&add_game](const QDir& directory, const QDir& root) {
|
||||
const auto entries = directory.entryInfoList(QDir::Files | QDir::NoSymLinks, QDir::Name);
|
||||
for (const auto& archive: entries) {
|
||||
const auto base = archive.absoluteFilePath();
|
||||
if (!Common::IsSupportedArchive(GameContent::ToPath(base))) {
|
||||
continue;
|
||||
}
|
||||
add_game(base, QDir::cleanPath(base), root.relativeFilePath(base),
|
||||
archive.completeBaseName(), true);
|
||||
}
|
||||
};
|
||||
|
||||
for (const auto& root_path: m_game_dirs) {
|
||||
QDir root(root_path);
|
||||
@@ -616,6 +696,7 @@ void ConfigurationListWidget::ScanGameDirectory() {
|
||||
continue;
|
||||
}
|
||||
|
||||
scan_archives(root, root);
|
||||
QList<QDir> pending_dirs;
|
||||
const auto root_subdirs =
|
||||
root.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks);
|
||||
@@ -625,74 +706,13 @@ void ConfigurationListWidget::ScanGameDirectory() {
|
||||
|
||||
while (!pending_dirs.isEmpty()) {
|
||||
QDir game_dir = pending_dirs.takeFirst();
|
||||
scan_archives(game_dir, root);
|
||||
|
||||
if (game_dir.exists(eboot_name)) {
|
||||
const QString game_path = NormalizeGameDirectory(game_dir.absolutePath());
|
||||
const QString legacy_game_path = root.relativeFilePath(game_dir.absolutePath());
|
||||
const QString game_key = PathKey(game_path);
|
||||
if (game_key.isEmpty() || found_games.contains(game_key)) {
|
||||
continue;
|
||||
}
|
||||
found_games.insert(game_key);
|
||||
|
||||
auto metadata = GetGameMetadata(
|
||||
game_dir.filePath(QStringLiteral("sce_sys/param.json")), game_dir.dirName());
|
||||
|
||||
auto info = std::make_unique<Configuration>();
|
||||
info->custom_settings =
|
||||
FindCustomInfo(&m_custom_infos, game_path, legacy_game_path) != nullptr;
|
||||
SetGameFiles(*info, game_dir.absolutePath(), game_path, metadata);
|
||||
const auto* compatibility = m_compatibility->Find(info->title_id);
|
||||
if (compatibility != nullptr) {
|
||||
info->game_status = compatibility->status;
|
||||
info->game_comment = compatibility->comment;
|
||||
}
|
||||
|
||||
if (auto* item = running_items.value(game_key); item != nullptr) {
|
||||
// Refresh metadata without losing the running row's identity.
|
||||
const QSignalBlocker status_blocker(item->GetStatusCombo());
|
||||
item->GetInfo().CopyGameInfoFrom(*info);
|
||||
item->Update();
|
||||
item->SetCompatibilityEditable(m_compatibility->IsLocal() &&
|
||||
!item->GetInfo().title_id.trimmed().isEmpty());
|
||||
continue;
|
||||
}
|
||||
|
||||
auto* item = new ConfigurationItem(std::move(info), m_ui->cfgs_list);
|
||||
item->SetCompatibilityEditable(m_compatibility->IsLocal() &&
|
||||
!item->GetInfo().title_id.trimmed().isEmpty());
|
||||
connect(item->GetStatusCombo(), &QComboBox::currentIndexChanged, item,
|
||||
[this, item](int /*index*/) {
|
||||
if (!m_compatibility->IsLocal()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& title_id = item->GetInfo().title_id;
|
||||
if (title_id.trimmed().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
item->GetInfo().game_status = static_cast<Configuration::GameStatus>(
|
||||
item->GetStatusCombo()->currentData().toInt());
|
||||
item->Update();
|
||||
m_compatibility->SetStatus(title_id, item->GetInfo().game_status);
|
||||
m_ui->cfgs_list->setCurrentItem(item);
|
||||
SelectItem(item);
|
||||
});
|
||||
connect(item->GetCommentEdit(), &QLineEdit::editingFinished, item, [this, item]() {
|
||||
if (!m_compatibility->IsLocal()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& title_id = item->GetInfo().title_id;
|
||||
if (title_id.trimmed().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
item->GetInfo().game_comment = item->GetCommentEdit()->text();
|
||||
item->Update();
|
||||
m_compatibility->SetComment(title_id, item->GetInfo().game_comment);
|
||||
m_ui->cfgs_list->setCurrentItem(item);
|
||||
SelectItem(item);
|
||||
});
|
||||
add_game(game_dir.absolutePath(), game_path, legacy_game_path, game_dir.dirName(),
|
||||
false);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -805,7 +825,8 @@ void ConfigurationListWidget::open_game_folder() {
|
||||
return;
|
||||
}
|
||||
|
||||
const QDir game_dir(item->GetInfo().basedir);
|
||||
const auto base = item->GetInfo().basedir;
|
||||
const QDir game_dir(GameContent::IsArchive(base) ? QFileInfo(base).absolutePath() : base);
|
||||
if (!game_dir.exists()) {
|
||||
QMessageBox::warning(this, tr("Open game folder"), tr("Game folder does not exist."));
|
||||
return;
|
||||
@@ -896,7 +917,7 @@ void ConfigurationListWidget::SelectItem(QTreeWidgetItem* witem) {
|
||||
m_ui->edit_button->setEnabled(!item->IsRunning());
|
||||
|
||||
m_selected_item = item;
|
||||
m_ui->cfgs_list->SetBackgroundImage(GetPic0Path(item->GetInfo()));
|
||||
m_ui->cfgs_list->SetBackgroundImage(item->GetInfo().basedir);
|
||||
|
||||
emit Select();
|
||||
}
|
||||
@@ -959,7 +980,8 @@ void ConfigurationListWidget::show_context_menu(const QPoint& pos) {
|
||||
|
||||
if (item != nullptr) {
|
||||
action_run->setDisabled(item->IsRunning());
|
||||
action_open_folder->setDisabled(!QDir(item->GetInfo().basedir).exists());
|
||||
const auto& base = item->GetInfo().basedir;
|
||||
action_open_folder->setDisabled(!QDir(base).exists() && !GameContent::IsArchive(base));
|
||||
action_view_trophies->setDisabled(!has_trophy_data);
|
||||
action_remove_save_data->setDisabled(item->IsRunning() || save_data_dirs.isEmpty());
|
||||
action_edit->setDisabled(item->IsRunning());
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#include "gameContent.h"
|
||||
|
||||
#include "common/archive.h"
|
||||
#include "common/file.h"
|
||||
#include "common/stringUtils.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
|
||||
#include <limits>
|
||||
|
||||
namespace GameContent {
|
||||
|
||||
std::filesystem::path ToPath(const QString& path) {
|
||||
#if defined(_WIN32)
|
||||
return std::filesystem::path(path.toStdWString());
|
||||
#else
|
||||
return std::filesystem::path(path.toStdString());
|
||||
#endif
|
||||
}
|
||||
|
||||
QString FromPath(const std::filesystem::path& path) {
|
||||
#if defined(_WIN32)
|
||||
return QString::fromStdWString(path.wstring());
|
||||
#else
|
||||
return QString::fromStdString(Common::PathToString(path));
|
||||
#endif
|
||||
}
|
||||
|
||||
bool IsArchive(const QString& base) {
|
||||
const QFileInfo info(base);
|
||||
return info.isFile() && Common::IsSupportedArchive(ToPath(base));
|
||||
}
|
||||
|
||||
std::filesystem::path Resolve(const QString& base, const QString& relative) {
|
||||
const auto root = ToPath(base);
|
||||
return IsArchive(base) ? Common::MakeArchivePath(root, ToPath(relative))
|
||||
: root / ToPath(relative);
|
||||
}
|
||||
|
||||
bool FileExists(const QString& base, const QString& relative) {
|
||||
return Common::File::IsFileExisting(Resolve(base, relative));
|
||||
}
|
||||
|
||||
QByteArray ReadFile(const QString& base, const QString& relative, uint64_t max_size) {
|
||||
return ReadPath(Resolve(base, relative), max_size);
|
||||
}
|
||||
|
||||
QByteArray ReadPath(const std::filesystem::path& path, uint64_t max_size) {
|
||||
Common::File file(path, Common::File::Mode::Read);
|
||||
if (file.IsInvalid()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto size = file.Size();
|
||||
if (size > max_size || size > static_cast<uint64_t>(std::numeric_limits<int>::max())) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QByteArray data(static_cast<int>(size), Qt::Uninitialized);
|
||||
uint32_t bytes_read = 0;
|
||||
file.Read(data.data(), static_cast<uint32_t>(size), &bytes_read);
|
||||
return bytes_read == size ? data : QByteArray {};
|
||||
}
|
||||
|
||||
QStringList ListFiles(const QString& base, const QString& relative) {
|
||||
const auto directory = Resolve(base, relative);
|
||||
const bool archive = Common::IsArchivePath(directory);
|
||||
QStringList result;
|
||||
for (const auto& entry: Common::File::GetDirEntries(directory)) {
|
||||
if (entry.is_file) {
|
||||
const auto path = FromPath(directory / Common::PathFromUtf8(entry.name));
|
||||
if (archive || !QFileInfo(path).isSymLink()) {
|
||||
result.append(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace GameContent
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "configuration.h"
|
||||
#include "configurationItem.h"
|
||||
#include "configurationListWidget.h"
|
||||
#include "gameContent.h"
|
||||
#include "patchesDialog.h"
|
||||
#include "updateChecker.h"
|
||||
|
||||
@@ -260,7 +261,7 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
|
||||
}
|
||||
|
||||
QString game = info.basedir;
|
||||
if (!info.elf.isEmpty()) {
|
||||
if (!info.elf.isEmpty() && !GameContent::IsArchive(info.basedir)) {
|
||||
game = QDir(info.basedir).filePath(info.elf);
|
||||
}
|
||||
args << "--game" << game;
|
||||
@@ -507,7 +508,7 @@ void MainDialogPrivate::Update() {
|
||||
if (run_enabled) {
|
||||
const auto& info = item->GetInfo();
|
||||
auto dir = info.basedir;
|
||||
run_enabled = !dir.isEmpty() && QDir(dir).exists();
|
||||
run_enabled = !dir.isEmpty() && (QDir(dir).exists() || GameContent::IsArchive(dir));
|
||||
}
|
||||
|
||||
m_ui->widget->SetRunEnabled(run_enabled);
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
#include "trophyViewerDialog.h"
|
||||
|
||||
#include "common/archive.h"
|
||||
#include "configuration.h"
|
||||
#include "gameContent.h"
|
||||
|
||||
#include <QAbstractItemView>
|
||||
#include <QBrush>
|
||||
#include <QByteArray>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QFont>
|
||||
#include <QHeaderView>
|
||||
@@ -39,12 +40,14 @@
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr quint32 UCP_MAGIC = 0xb228c60a;
|
||||
constexpr quint32 UCP_VERSION = 1;
|
||||
constexpr int UCP_HEADER_LEN = 0x40;
|
||||
constexpr int UCP_TOC_SKIP = 0x20;
|
||||
constexpr int UCP_ENTRY_LEN = 0x40;
|
||||
constexpr int UCP_NAME_LEN = 0x20;
|
||||
constexpr quint32 UCP_MAGIC = 0xb228c60a;
|
||||
constexpr quint32 UCP_VERSION = 1;
|
||||
constexpr int UCP_HEADER_LEN = 0x40;
|
||||
constexpr int UCP_TOC_SKIP = 0x20;
|
||||
constexpr int UCP_ENTRY_LEN = 0x40;
|
||||
constexpr int UCP_NAME_LEN = 0x20;
|
||||
constexpr quint32 UCP_MAX_FILES = 4096;
|
||||
constexpr quint64 UCP_MAX_EXTRACTED_SIZE = quint64 {64} << 20u;
|
||||
|
||||
struct UcpEntry {
|
||||
QString name;
|
||||
@@ -98,14 +101,22 @@ static QString ReadFixedString(const QByteArray& data, qsizetype offset, qsizety
|
||||
return QString::fromLatin1(data.constData() + offset, len);
|
||||
}
|
||||
|
||||
static bool IsUsedUcpEntry(const QString& name) {
|
||||
const auto lower = name.toCaseFolded();
|
||||
return lower == QStringLiteral("tropconf.json") || lower == QStringLiteral("tropmeta.json") ||
|
||||
(lower.startsWith(QStringLiteral("tropmeta_")) &&
|
||||
lower.endsWith(QStringLiteral(".json"))) ||
|
||||
(lower.startsWith(QStringLiteral("trop")) && lower.endsWith(QStringLiteral(".png")));
|
||||
}
|
||||
|
||||
static bool ReadUcp(const QString& file_name, QMap<QString, QByteArray>& files, QString& error) {
|
||||
QFile file(file_name);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
const QByteArray data =
|
||||
GameContent::ReadPath(GameContent::ToPath(file_name), GameContent::MaxTrophyPackageSize);
|
||||
if (data.isEmpty()) {
|
||||
error = QObject::tr("Could not open %1").arg(QDir::toNativeSeparators(file_name));
|
||||
return false;
|
||||
}
|
||||
|
||||
const QByteArray data = file.readAll();
|
||||
if (data.size() < UCP_HEADER_LEN) {
|
||||
error = QObject::tr("%1 is too small to be a trophy package.")
|
||||
.arg(QFileInfo(file_name).fileName());
|
||||
@@ -127,14 +138,18 @@ static bool ReadUcp(const QString& file_name, QMap<QString, QByteArray>& files,
|
||||
}
|
||||
|
||||
const auto declared_size = qFromBigEndian<quint64>(data.constData() + 0x08);
|
||||
if (declared_size > static_cast<quint64>(data.size())) {
|
||||
if (declared_size < UCP_HEADER_LEN || declared_size > static_cast<quint64>(data.size())) {
|
||||
error = QObject::tr("%1 is truncated.").arg(QFileInfo(file_name).fileName());
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto file_count = qFromBigEndian<quint32>(data.constData() + 0x10);
|
||||
if (file_count > UCP_MAX_FILES) {
|
||||
error = QObject::tr("%1 contains too many files.").arg(QFileInfo(file_name).fileName());
|
||||
return false;
|
||||
}
|
||||
const auto toc_offset = static_cast<quint64>(qFromBigEndian<quint32>(data.constData() + 0x14));
|
||||
const auto data_size = static_cast<quint64>(data.size());
|
||||
const auto data_size = declared_size;
|
||||
|
||||
const quint64 table_size = UCP_TOC_SKIP + static_cast<quint64>(file_count) * UCP_ENTRY_LEN;
|
||||
if (toc_offset > data_size || table_size > data_size - toc_offset ||
|
||||
@@ -144,6 +159,7 @@ static bool ReadUcp(const QString& file_name, QMap<QString, QByteArray>& files,
|
||||
return false;
|
||||
}
|
||||
|
||||
quint64 extracted_size = 0;
|
||||
for (quint32 i = 0; i < file_count; i++) {
|
||||
const auto entry_offset = static_cast<qsizetype>(toc_offset + UCP_TOC_SKIP +
|
||||
static_cast<quint64>(i) * UCP_ENTRY_LEN);
|
||||
@@ -161,9 +177,22 @@ static bool ReadUcp(const QString& file_name, QMap<QString, QByteArray>& files,
|
||||
.arg(QFileInfo(file_name).fileName(), entry.name);
|
||||
return false;
|
||||
}
|
||||
if (!IsUsedUcpEntry(entry.name)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto entry_limit = entry.name.endsWith(QStringLiteral(".png"), Qt::CaseInsensitive)
|
||||
? GameContent::MaxImageSize
|
||||
: GameContent::MaxMetadataSize;
|
||||
if (entry.size > entry_limit || entry.size > UCP_MAX_EXTRACTED_SIZE - extracted_size) {
|
||||
error = QObject::tr("%1 contains an oversized entry for %2.")
|
||||
.arg(QFileInfo(file_name).fileName(), entry.name);
|
||||
return false;
|
||||
}
|
||||
|
||||
files.insert(entry.name.toCaseFolded(), data.mid(static_cast<qsizetype>(entry.offset),
|
||||
static_cast<qsizetype>(entry.size)));
|
||||
extracted_size += entry.size;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -392,30 +421,31 @@ static QStringList FindTrophyFiles(const Configuration* info) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const QDir trophy_dir(QDir(info->basedir).filePath(QStringLiteral("sce_sys/trophy2")));
|
||||
if (!trophy_dir.exists()) {
|
||||
return {};
|
||||
QStringList files;
|
||||
for (const auto& file:
|
||||
GameContent::ListFiles(info->basedir, QStringLiteral("sce_sys/trophy2"))) {
|
||||
const auto name = QFileInfo(file).fileName();
|
||||
if (name.startsWith(QStringLiteral("trophy"), Qt::CaseInsensitive) &&
|
||||
name.endsWith(QStringLiteral(".ucp"), Qt::CaseInsensitive)) {
|
||||
files.append(file);
|
||||
}
|
||||
}
|
||||
|
||||
const auto files =
|
||||
trophy_dir.entryInfoList({QStringLiteral("Trophy*.ucp"), QStringLiteral("trophy*.ucp")},
|
||||
QDir::Files | QDir::NoSymLinks, QDir::Name | QDir::IgnoreCase);
|
||||
files.sort(Qt::CaseInsensitive);
|
||||
|
||||
QStringList trophy_files;
|
||||
QSet<QString> seen;
|
||||
for (const auto& file: files) {
|
||||
auto key = file.canonicalFilePath();
|
||||
auto key = QFileInfo(file).canonicalFilePath();
|
||||
if (key.isEmpty()) {
|
||||
key = file.absoluteFilePath();
|
||||
key = file;
|
||||
}
|
||||
key = QDir::cleanPath(key).toCaseFolded();
|
||||
|
||||
if (!seen.contains(key)) {
|
||||
seen.insert(key);
|
||||
trophy_files.append(file.absoluteFilePath());
|
||||
trophy_files.append(file);
|
||||
}
|
||||
}
|
||||
|
||||
return trophy_files;
|
||||
}
|
||||
|
||||
@@ -506,6 +536,7 @@ void TrophyViewerDialog::ShowForGame(const Configuration* info, QWidget* parent)
|
||||
}
|
||||
|
||||
bool TrophyViewerDialog::LoadGame(const Configuration& info, QString& error) {
|
||||
const auto reader = Common::OpenArchive(GameContent::ToPath(info.basedir));
|
||||
const auto trophy_files = FindTrophyFiles(&info);
|
||||
if (trophy_files.isEmpty()) {
|
||||
error = tr("No trophy package found in sce_sys/trophy2.");
|
||||
|
||||
Reference in New Issue
Block a user