Rebase: Make archive detection more robust and add it to the CLI (#1560)

* Rebase/recreate my changes and add MIME support

This commit recreates the changes proposed in #1394 on top of the
current master (b069a2acf1).
This also adds support for determining filetypes using the MIME database
provided by `QMimeDatabase`.

* Move member syntax warning to a more appropriate place

* Deduplicate member syntax warning

* Change warning from "vertical bars" to "|"

* Conform brace placement to coding style

* Fix QFileDialog filter when ArchiveExtensions is empty

* Final cleanup and fixes

- Changes the NDS and GBA ROM MIME-Type constants to QStrings.
- Removes a leftover warning message.
- Uses Type() syntax instead of Type{} syntax for temporaries.

* Explain the origin of the supported archive list

Co-authored-by: Jan Felix Langenbach <insert-penguin@protonmail.com>
This commit is contained in:
Janfel
2023-01-18 00:49:18 +01:00
committed by GitHub
parent d83172e595
commit 3e02d3ff76
4 changed files with 305 additions and 163 deletions

View File

@ -57,9 +57,9 @@ CommandLineOptions* ManageArgs(QApplication& melon)
default: default:
printf("Too many positional arguments; ignoring 3 onwards\n"); printf("Too many positional arguments; ignoring 3 onwards\n");
case 2: case 2:
options->gbaRomPath = QStringList(posargs[1]); options->gbaRomPath = posargs[1];
case 1: case 1:
options->dsRomPath = QStringList(posargs[0]); options->dsRomPath = posargs[0];
case 0: case 0:
break; break;
} }
@ -67,7 +67,7 @@ CommandLineOptions* ManageArgs(QApplication& melon)
QString bootMode = parser.value("boot"); QString bootMode = parser.value("boot");
if (bootMode == "auto") if (bootMode == "auto")
{ {
options->boot = posargs.size() > 0; options->boot = !posargs.empty();
} }
else if (bootMode == "always") else if (bootMode == "always")
{ {
@ -86,45 +86,25 @@ CommandLineOptions* ManageArgs(QApplication& melon)
#ifdef ARCHIVE_SUPPORT_ENABLED #ifdef ARCHIVE_SUPPORT_ENABLED
if (parser.isSet("archive-file")) if (parser.isSet("archive-file"))
{ {
if (options->dsRomPath.isEmpty()) if (options->dsRomPath.has_value())
{ {
options->errorsToDisplay += "Option -a/--archive-file given, but no archive specified!"; options->dsRomArchivePath = parser.value("archive-file");
} }
else else
{ {
options->dsRomPath += parser.value("archive-file"); options->errorsToDisplay += "Option -a/--archive-file given, but no archive specified!";
}
}
else if (!options->dsRomPath.isEmpty())
{
//TODO-CLI: try to automatically find ROM
QStringList paths = options->dsRomPath[0].split("|");
if (paths.size() >= 2)
{
printf("Warning: use the a.zip|b.nds format at your own risk!\n");
options->dsRomPath = paths;
} }
} }
if (parser.isSet("archive-file-gba")) if (parser.isSet("archive-file-gba"))
{ {
if (options->gbaRomPath.isEmpty()) if (options->gbaRomPath.has_value())
{ {
options->errorsToDisplay += "Option -A/--archive-file-gba given, but no archive specified!"; options->gbaRomArchivePath = parser.value("archive-file-gba");
} }
else else
{ {
options->gbaRomPath += parser.value("archive-file-gba"); options->errorsToDisplay += "Option -A/--archive-file-gba given, but no archive specified!";
}
}
else if (!options->gbaRomPath.isEmpty())
{
//TODO-CLI: try to automatically find ROM
QStringList paths = options->gbaRomPath[0].split("|");
if (paths.size() >= 2)
{
printf("Warning: use the a.zip|b.gba format at your own risk!\n");
options->gbaRomPath = paths;
} }
} }
#endif #endif

View File

@ -22,14 +22,18 @@
#include <QApplication> #include <QApplication>
#include <QStringList> #include <QStringList>
#include <optional>
namespace CLI { namespace CLI {
struct CommandLineOptions struct CommandLineOptions
{ {
QStringList errorsToDisplay = {}; QStringList errorsToDisplay = {};
QStringList dsRomPath; std::optional<QString> dsRomPath;
QStringList gbaRomPath; std::optional<QString> dsRomArchivePath;
std::optional<QString> gbaRomPath;
std::optional<QString> gbaRomArchivePath;
bool fullscreen; bool fullscreen;
bool boot; bool boot;
}; };

View File

@ -21,6 +21,7 @@
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include <optional>
#include <vector> #include <vector>
#include <string> #include <string>
#include <algorithm> #include <algorithm>
@ -29,6 +30,7 @@
#include <QApplication> #include <QApplication>
#include <QMessageBox> #include <QMessageBox>
#include <QMenuBar> #include <QMenuBar>
#include <QMimeDatabase>
#include <QFileDialog> #include <QFileDialog>
#include <QInputDialog> #include <QInputDialog>
#include <QPaintEvent> #include <QPaintEvent>
@ -99,6 +101,55 @@
// TODO: uniform variable spelling // TODO: uniform variable spelling
const QString NdsRomMimeType = "application/x-nintendo-ds-rom";
const QStringList NdsRomExtensions { ".nds", ".srl", ".dsi", ".ids" };
const QString GbaRomMimeType = "application/x-gba-rom";
const QStringList GbaRomExtensions { ".gba", ".agb" };
// This list of supported archive formats is based on libarchive(3) version 3.6.2 (2022-12-09).
const QStringList ArchiveMimeTypes
{
#ifdef ARCHIVE_SUPPORT_ENABLED
"application/zip",
"application/x-7z-compressed",
"application/vnd.rar", // *.rar
"application/x-tar",
"application/x-compressed-tar", // *.tar.gz
"application/x-xz-compressed-tar",
"application/x-bzip-compressed-tar",
"application/x-lz4-compressed-tar",
"application/x-zstd-compressed-tar",
"application/x-tarz", // *.tar.Z
"application/x-lzip-compressed-tar",
"application/x-lzma-compressed-tar",
"application/x-lrzip-compressed-tar",
"application/x-tzo", // *.tar.lzo
#endif
};
const QStringList ArchiveExtensions
{
#ifdef ARCHIVE_SUPPORT_ENABLED
".zip", ".7z", ".rar", ".tar",
".tar.gz", ".tgz",
".tar.xz", ".txz",
".tar.bz2", ".tbz2",
".tar.lz4", ".tlz4",
".tar.zst", ".tzst",
".tar.Z", ".taz",
".tar.lz",
".tar.lzma", ".tlz",
".tar.lrz", ".tlrz",
".tar.lzo", ".tzo",
#endif
};
bool RunningSomething; bool RunningSomething;
MainWindow* mainWindow; MainWindow* mainWindow;
@ -1418,6 +1469,65 @@ void ScreenPanelGL::onScreenLayoutChanged()
setupScreenLayout(); setupScreenLayout();
} }
static bool FileExtensionInList(const QString& filename, const QStringList& extensions, Qt::CaseSensitivity cs = Qt::CaseInsensitive)
{
return std::any_of(extensions.cbegin(), extensions.cend(), [&](const auto& ext) {
return filename.endsWith(ext, cs);
});
}
static bool MimeTypeInList(const QMimeType& mimetype, const QStringList& superTypeNames)
{
return std::any_of(superTypeNames.cbegin(), superTypeNames.cend(), [&](const auto& superTypeName) {
return mimetype.inherits(superTypeName);
});
}
static bool NdsRomByExtension(const QString& filename)
{
return FileExtensionInList(filename, NdsRomExtensions);
}
static bool GbaRomByExtension(const QString& filename)
{
return FileExtensionInList(filename, GbaRomExtensions);
}
static bool SupportedArchiveByExtension(const QString& filename)
{
return FileExtensionInList(filename, ArchiveExtensions);
}
static bool NdsRomByMimetype(const QMimeType& mimetype)
{
return mimetype.inherits(NdsRomMimeType);
}
static bool GbaRomByMimetype(const QMimeType& mimetype)
{
return mimetype.inherits(GbaRomMimeType);
}
static bool SupportedArchiveByMimetype(const QMimeType& mimetype)
{
return MimeTypeInList(mimetype, ArchiveMimeTypes);
}
static bool FileIsSupportedFiletype(const QString& filename, bool insideArchive = false)
{
if (NdsRomByExtension(filename) || GbaRomByExtension(filename) || SupportedArchiveByExtension(filename))
return true;
const auto matchmode = insideArchive ? QMimeDatabase::MatchExtension : QMimeDatabase::MatchDefault;
const QMimeType mimetype = QMimeDatabase().mimeTypeForFile(filename, matchmode);
return NdsRomByMimetype(mimetype) || GbaRomByMimetype(mimetype) || SupportedArchiveByMimetype(mimetype);
}
#ifndef _WIN32 #ifndef _WIN32
static int signalFd[2]; static int signalFd[2];
QSocketNotifier *signalSn; QSocketNotifier *signalSn;
@ -2014,15 +2124,9 @@ void MainWindow::dragEnterEvent(QDragEnterEvent* event)
QString filename = urls.at(0).toLocalFile(); QString filename = urls.at(0).toLocalFile();
QStringList acceptedExts{".nds", ".srl", ".dsi", ".gba", ".rar", if (FileIsSupportedFiletype(filename))
".zip", ".7z", ".tar", ".tar.gz", ".tar.xz", ".tar.bz2"};
for (const QString &ext : acceptedExts)
{
if (filename.endsWith(ext, Qt::CaseInsensitive))
event->acceptProposedAction(); event->acceptProposedAction();
} }
}
void MainWindow::dropEvent(QDropEvent* event) void MainWindow::dropEvent(QDropEvent* event)
{ {
@ -2031,9 +2135,6 @@ void MainWindow::dropEvent(QDropEvent* event)
QList<QUrl> urls = event->mimeData()->urls(); QList<QUrl> urls = event->mimeData()->urls();
if (urls.count() > 1) return; // not handling more than one file at once if (urls.count() > 1) return; // not handling more than one file at once
QString filename = urls.at(0).toLocalFile();
QStringList arcexts{".zip", ".7z", ".rar", ".tar", ".tar.gz", ".tar.xz", ".tar.bz2"};
emuThread->emuPause(); emuThread->emuPause();
if (!verifySetup()) if (!verifySetup())
@ -2042,29 +2143,44 @@ void MainWindow::dropEvent(QDropEvent* event)
return; return;
} }
for (const QString &ext : arcexts) const QStringList file = splitArchivePath(urls.at(0).toLocalFile(), false);
{ if (file.isEmpty())
if (filename.endsWith(ext, Qt::CaseInsensitive))
{
QString arcfile = pickFileFromArchive(filename);
if (arcfile.isEmpty())
{ {
emuThread->emuUnpause(); emuThread->emuUnpause();
return; return;
} }
filename += "|" + arcfile; const QString filename = file.last();
} const bool romInsideArchive = file.size() > 1;
const auto matchMode = romInsideArchive ? QMimeDatabase::MatchExtension : QMimeDatabase::MatchDefault;
const QMimeType mimetype = QMimeDatabase().mimeTypeForFile(filename, matchMode);
if (NdsRomByExtension(filename) || NdsRomByMimetype(mimetype))
{
if (!ROMManager::LoadROM(file, true))
{
// TODO: better error reporting?
QMessageBox::critical(this, "melonDS", "Failed to load the DS ROM.");
emuThread->emuUnpause();
return;
} }
QStringList file = filename.split('|'); const QString barredFilename = file.join('|');
recentFileList.removeAll(barredFilename);
recentFileList.prepend(barredFilename);
updateRecentFilesMenu();
if (filename.endsWith(".gba", Qt::CaseInsensitive)) NDS::Start();
emuThread->emuRun();
updateCartInserted(false);
}
else if (GbaRomByExtension(filename) || GbaRomByMimetype(mimetype))
{ {
if (!ROMManager::LoadGBAROM(file)) if (!ROMManager::LoadGBAROM(file))
{ {
// TODO: better error reporting? // TODO: better error reporting?
QMessageBox::critical(this, "melonDS", "Failed to load the ROM."); QMessageBox::critical(this, "melonDS", "Failed to load the GBA ROM.");
emuThread->emuUnpause(); emuThread->emuUnpause();
return; return;
} }
@ -2075,23 +2191,10 @@ void MainWindow::dropEvent(QDropEvent* event)
} }
else else
{ {
if (!ROMManager::LoadROM(file, true)) QMessageBox::critical(this, "melonDS", "The file could not be recognized as a DS or GBA ROM.");
{
// TODO: better error reporting?
QMessageBox::critical(this, "melonDS", "Failed to load the ROM.");
emuThread->emuUnpause(); emuThread->emuUnpause();
return; return;
} }
recentFileList.removeAll(filename);
recentFileList.prepend(filename);
updateRecentFilesMenu();
NDS::Start();
emuThread->emuRun();
updateCartInserted(false);
}
} }
void MainWindow::focusInEvent(QFocusEvent* event) void MainWindow::focusInEvent(QFocusEvent* event)
@ -2188,101 +2291,129 @@ bool MainWindow::preloadROMs(QStringList file, QStringList gbafile, bool boot)
return true; return true;
} }
QStringList MainWindow::splitArchivePath(const QString& filename, bool useMemberSyntax)
{
if (filename.isEmpty()) return {};
#ifdef ARCHIVE_SUPPORT_ENABLED
if (useMemberSyntax)
{
const QStringList filenameParts = filename.split('|');
if (filenameParts.size() > 2)
{
QMessageBox::warning(this, "melonDS", "This path contains too many '|'.");
return {};
}
if (filenameParts.size() == 2)
{
const QString archive = filenameParts.at(0);
if (!QFileInfo(archive).exists())
{
QMessageBox::warning(this, "melonDS", "This archive does not exist.");
return {};
}
const QString subfile = filenameParts.at(1);
if (!Archive::ListArchive(archive).contains(subfile))
{
QMessageBox::warning(this, "melonDS", "This archive does not contain the desired file.");
return {};
}
return filenameParts;
}
}
#endif
if (!QFileInfo(filename).exists())
{
QMessageBox::warning(this, "melonDS", "This ROM file does not exist.");
return {};
}
#ifdef ARCHIVE_SUPPORT_ENABLED
if (SupportedArchiveByExtension(filename)
|| SupportedArchiveByMimetype(QMimeDatabase().mimeTypeForFile(filename)))
{
const QString subfile = pickFileFromArchive(filename);
if (subfile.isEmpty())
return {};
return { filename, subfile };
}
#endif
return { filename };
}
QString MainWindow::pickFileFromArchive(QString archiveFileName) QString MainWindow::pickFileFromArchive(QString archiveFileName)
{ {
QVector<QString> archiveROMList = Archive::ListArchive(archiveFileName); QVector<QString> archiveROMList = Archive::ListArchive(archiveFileName);
QString romFileName = ""; // file name inside archive if (archiveROMList.size() <= 1)
if (archiveROMList.size() > 2)
{ {
if (!archiveROMList.isEmpty() && archiveROMList.at(0) == "OK")
QMessageBox::warning(this, "melonDS", "This archive is empty.");
else
QMessageBox::critical(this, "melonDS", "This archive could not be read. It may be corrupt or you don't have the permissions.");
return QString();
}
archiveROMList.removeFirst(); archiveROMList.removeFirst();
bool ok; const auto notSupportedRom = [&](const auto& filename){
QString toLoad = QInputDialog::getItem(this, "melonDS", if (NdsRomByExtension(filename) || GbaRomByExtension(filename))
"This archive contains multiple files. Select which ROM you want to load.", archiveROMList.toList(), 0, false, &ok); return false;
if (!ok) // User clicked on cancel const QMimeType mimetype = QMimeDatabase().mimeTypeForFile(filename, QMimeDatabase::MatchExtension);
return !(NdsRomByMimetype(mimetype) || GbaRomByMimetype(mimetype));
};
archiveROMList.erase(std::remove_if(archiveROMList.begin(), archiveROMList.end(), notSupportedRom),
archiveROMList.end());
if (archiveROMList.isEmpty())
{
QMessageBox::warning(this, "melonDS", "This archive does not contain any supported ROMs.");
return QString(); return QString();
romFileName = toLoad;
}
else if (archiveROMList.size() == 2)
{
romFileName = archiveROMList.at(1);
}
else if ((archiveROMList.size() == 1) && (archiveROMList[0] == QString("OK")))
{
QMessageBox::warning(this, "melonDS", "This archive is empty.");
}
else
{
QMessageBox::critical(this, "melonDS", "This archive could not be read. It may be corrupt or you don't have the permissions.");
} }
return romFileName; if (archiveROMList.size() == 1)
return archiveROMList.first();
bool ok;
const QString toLoad = QInputDialog::getItem(
this, "melonDS",
"This archive contains multiple files. Select which ROM you want to load.",
archiveROMList.toList(), 0, false, &ok
);
if (ok) return toLoad;
// User clicked on cancel
return QString();
} }
QStringList MainWindow::pickROM(bool gba) QStringList MainWindow::pickROM(bool gba)
{ {
QString console; const QString console = gba ? "GBA" : "DS";
QStringList romexts; const QStringList& romexts = gba ? GbaRomExtensions : NdsRomExtensions;
QStringList arcexts{"*.zip", "*.7z", "*.rar", "*.tar", "*.tar.gz", "*.tar.xz", "*.tar.bz2"};
QStringList ret;
if (gba) static const QString filterSuffix = ArchiveExtensions.empty()
{ ? ");;Any file (*.*)"
console = "GBA"; : " *" + ArchiveExtensions.join(" *") + ");;Any file (*.*)";
romexts.append("*.gba");
}
else
{
console = "DS";
romexts.append({"*.nds", "*.dsi", "*.ids", "*.srl"});
}
QString filter = romexts.join(' ') + " " + arcexts.join(' '); const QString filename = QFileDialog::getOpenFileName(
filter = console + " ROMs (" + filter + ");;Any file (*.*)"; this, "Open " + console + " ROM",
QString filename = QFileDialog::getOpenFileName(this,
"Open "+console+" ROM",
QString::fromStdString(Config::LastROMFolder), QString::fromStdString(Config::LastROMFolder),
filter); console + " ROMs (*" + romexts.join(" *") + filterSuffix
if (filename.isEmpty()) );
return ret;
int pos = filename.length() - 1; if (filename.isEmpty()) return {};
while (filename[pos] != '/' && filename[pos] != '\\' && pos > 0) pos--;
QString path_dir = filename.left(pos);
QString path_file = filename.mid(pos+1);
Config::LastROMFolder = path_dir.toStdString(); Config::LastROMFolder = QFileInfo(filename).dir().path().toStdString();
return splitArchivePath(filename, false);
bool isarc = false;
for (const auto& ext : arcexts)
{
int l = ext.length() - 1;
if (path_file.right(l).toLower() == ext.right(l))
{
isarc = true;
break;
}
}
if (isarc)
{
path_file = pickFileFromArchive(filename);
if (path_file.isEmpty())
return ret;
ret.append(filename);
ret.append(path_file);
}
else
{
ret.append(filename);
}
return ret;
} }
void MainWindow::updateCartInserted(bool gba) void MainWindow::updateCartInserted(bool gba)
@ -2405,7 +2536,6 @@ void MainWindow::onClickRecentFile()
{ {
QAction *act = (QAction *)sender(); QAction *act = (QAction *)sender();
QString filename = act->data().toString(); QString filename = act->data().toString();
QStringList file = filename.split('|');
emuThread->emuPause(); emuThread->emuPause();
@ -2415,6 +2545,13 @@ void MainWindow::onClickRecentFile()
return; return;
} }
const QStringList file = splitArchivePath(filename, true);
if (file.isEmpty())
{
emuThread->emuUnpause();
return;
}
if (!ROMManager::LoadROM(file, true)) if (!ROMManager::LoadROM(file, true))
{ {
// TODO: better error reporting? // TODO: better error reporting?
@ -3237,7 +3374,8 @@ bool MelonApplication::event(QEvent *event)
QFileOpenEvent *openEvent = static_cast<QFileOpenEvent*>(event); QFileOpenEvent *openEvent = static_cast<QFileOpenEvent*>(event);
emuThread->emuPause(); emuThread->emuPause();
if (!mainWindow->preloadROMs(openEvent->file().split("|"), {}, true)) const QStringList file = mainWindow->splitArchivePath(openEvent->file(), true);
if (!mainWindow->preloadROMs(file, {}, true))
emuThread->emuUnpause(); emuThread->emuUnpause();
} }
@ -3379,7 +3517,26 @@ int main(int argc, char** argv)
QObject::connect(&melon, &QApplication::applicationStateChanged, mainWindow, &MainWindow::onAppStateChanged); QObject::connect(&melon, &QApplication::applicationStateChanged, mainWindow, &MainWindow::onAppStateChanged);
mainWindow->preloadROMs(options->dsRomPath, options->gbaRomPath, options->boot); bool memberSyntaxUsed = false;
const auto prepareRomPath = [&](const std::optional<QString>& romPath, const std::optional<QString>& romArchivePath) -> QStringList
{
if (!romPath.has_value())
return {};
if (romArchivePath.has_value())
return { *romPath, *romArchivePath };
const QStringList path = mainWindow->splitArchivePath(*romPath, true);
if (path.size() > 1) memberSyntaxUsed = true;
return path;
};
const QStringList dsfile = prepareRomPath(options->dsRomPath, options->dsRomArchivePath);
const QStringList gbafile = prepareRomPath(options->gbaRomPath, options->gbaRomArchivePath);
if (memberSyntaxUsed) printf("Warning: use the a.zip|b.nds format at your own risk!\n");
mainWindow->preloadROMs(dsfile, gbafile, options->boot);
int ret = melon.exec(); int ret = melon.exec();

View File

@ -239,6 +239,7 @@ public:
GL::Context* getOGLContext(); GL::Context* getOGLContext();
bool preloadROMs(QStringList file, QStringList gbafile, bool boot); bool preloadROMs(QStringList file, QStringList gbafile, bool boot);
QStringList splitArchivePath(const QString& filename, bool useMemberSyntax);
void onAppStateChanged(Qt::ApplicationState state); void onAppStateChanged(Qt::ApplicationState state);