qt5base-lts/tests/auto/tools/rcc/tst_rcc.cpp
Thiago Macieira d20c980576 Resources: reject compressed content we can't decompress
This solution is composed of two features:

1) C++ code generated by RCC uses two symbols exported from QtCore that
are only present if the feature was compiled in. If the feature was not
compiled in, this will cause a linker error either at build time or at
load time (if they were functions, the error could be at runtime).

2) Binary files generated by RCC have a new header field containing
flags. We're currently using two flags, one for Zlib and one for
Zstandard.

This means we now have binary RCC format version 3.

Change-Id: I42a48bd64ccc41aebf84fffd156545fb6a4f72d9
Reviewed-by: Oswald Buddenhagen <oswald.buddenhagen@qt.io>
Reviewed-by: Lars Knoll <lars.knoll@qt.io>
2018-12-12 16:36:09 +00:00

420 lines
15 KiB
C++

/****************************************************************************
**
** Copyright (C) 2012 Giuseppe D'Angelo <dangelog@gmail.com>
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the test suite of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include <QtTest/QtTest>
#include <QtCore/QString>
#include <QtCore/QCoreApplication>
#include <QtCore/QByteArray>
#include <QtCore/QDir>
#include <QtCore/QFile>
#include <QtCore/QProcess>
#include <QtCore/QDirIterator>
#include <QtCore/QMap>
#include <QtCore/QList>
#include <QtCore/QResource>
#include <QtCore/QLocale>
#include <QtCore/QtGlobal>
#include <algorithm>
typedef QMap<QString, QString> QStringMap;
Q_DECLARE_METATYPE(QStringMap)
class tst_rcc : public QObject
{
Q_OBJECT
private slots:
void initTestCase();
void rcc_data();
void rcc();
void binary_data();
void binary();
void readback_data();
void readback();
void cleanupTestCase();
private:
QString m_rcc;
};
void tst_rcc::initTestCase()
{
// rcc uses a QHash to store files in the resource system.
// we must force a certain hash order when testing or tst_rcc will fail, see QTBUG-25078
QVERIFY(qputenv("QT_RCC_TEST", "1"));
m_rcc = QLibraryInfo::location(QLibraryInfo::BinariesPath) + QLatin1String("/rcc");
}
QString findExpectedFile(const QString &base)
{
QString expectedrccfile = base;
// Must be updated with each minor release.
if (QFileInfo(expectedrccfile + QLatin1String(".450")).exists())
expectedrccfile += QLatin1String(".450");
return expectedrccfile;
}
static QString doCompare(const QStringList &actual, const QStringList &expected)
{
if (actual.size() != expected.size()) {
return QString("Length count different: actual: %1, expected: %2")
.arg(actual.size()).arg(expected.size());
}
QByteArray ba;
for (int i = 0, n = expected.size(); i != n; ++i) {
QString expectedLine = expected.at(i);
if (expectedLine.startsWith("IGNORE:"))
continue;
if (expectedLine.startsWith("TIMESTAMP:")) {
const QString relativePath = expectedLine.mid(strlen("TIMESTAMP:"));
const quint64 timeStamp = QFileInfo(relativePath).lastModified().toMSecsSinceEpoch();
expectedLine.clear();
for (int shift = 56; shift >= 0; shift -= 8) {
expectedLine.append(QLatin1String("0x"));
expectedLine.append(QString::number(quint8(timeStamp >> shift), 16));
expectedLine.append(QLatin1Char(','));
}
}
if (expectedLine != actual.at(i)) {
qDebug() << "LINES" << i << "DIFFER";
ba.append(
"\n<<<<<< actual\n" + actual.at(i) + "\n======\n" + expectedLine
+ "\n>>>>>> expected\n"
);
}
}
return ba;
}
void tst_rcc::rcc_data()
{
QTest::addColumn<QString>("directory");
QTest::addColumn<QString>("qrcfile");
QTest::addColumn<QString>("expected");
QString dataPath = QFINDTESTDATA("data/images/");
if (dataPath.isEmpty())
QFAIL("data path not found");
QTest::newRow("images") << dataPath << "images.qrc" << "images.expected";
QString sizesPath = QFINDTESTDATA("data/sizes/");
if (sizesPath.isEmpty())
QFAIL("data path not found");
QTest::newRow("size-0") << sizesPath << "size-0.qrc" << "size-0.expected";
QTest::newRow("size-1") << sizesPath << "size-1.qrc" << "size-1.expected";
QTest::newRow("size-2-0-35-1") << sizesPath << "size-2-0-35-1.qrc" << "size-2-0-35-1.expected";
}
void tst_rcc::rcc()
{
QFETCH(QString, directory);
QFETCH(QString, qrcfile);
QFETCH(QString, expected);
if (!QDir::setCurrent(directory)) {
QString message = QString::fromLatin1("Unable to cd from '%1' to '%2'").arg(QDir::currentPath(), directory);
QFAIL(qPrintable(message));
}
// If the file expectedoutput.txt exists, compare the
// console output with the content of that file
const QString expected2 = findExpectedFile(expected);
QFile expectedFile(expected2);
if (!expectedFile.exists()) {
qDebug() << "NO EXPECTATIONS? " << expected2;
return;
}
// Launch; force no compression, otherwise the output would be different
// depending on the compression algorithm we're using
QProcess process;
process.start(m_rcc, { "-no-compress", qrcfile });
if (!process.waitForFinished()) {
const QString path = QString::fromLocal8Bit(qgetenv("PATH"));
QString message = QString::fromLatin1("'%1' could not be found when run from '%2'. Path: '%3' ").
arg(m_rcc, QDir::currentPath(), path);
QFAIL(qPrintable(message));
}
const QChar cr = QLatin1Char('\r');
const QString err = QString::fromLocal8Bit(process.readAllStandardError()).remove(cr);
const QString out = QString::fromLatin1(process.readAllStandardOutput()).remove(cr);
if (!err.isEmpty()) {
qDebug() << "UNEXPECTED STDERR CONTENTS: " << err;
QFAIL("UNEXPECTED STDERR CONTENTS");
}
const QChar nl = QLatin1Char('\n');
const QStringList actualLines = out.split(nl);
QVERIFY(expectedFile.open(QIODevice::ReadOnly|QIODevice::Text));
const QStringList expectedLines = QString::fromLatin1(expectedFile.readAll()).split(nl);
const QString diff = doCompare(actualLines, expectedLines);
if (diff.size())
QFAIL(qPrintable(diff));
}
static void createRccBinaryData(const QString &rcc, const QString &baseDir,
const QString &qrcFileName, const QString &rccFileName)
{
QString currentDir = QDir::currentPath();
QDir::setCurrent(baseDir);
// same as above: force no compression
QProcess rccProcess;
rccProcess.start(rcc, { "-binary", "-no-compress", "-o", rccFileName, qrcFileName });
bool ok = rccProcess.waitForFinished();
if (!ok) {
QString errorString = QString::fromLatin1("Could not start rcc (is it in PATH?): %1").arg(rccProcess.errorString());
QFAIL(qPrintable(errorString));
}
QByteArray output = rccProcess.readAllStandardOutput();
if (!output.isEmpty()) {
QString errorMessage = QString::fromLatin1("rcc stdout: %1").arg(QString::fromLocal8Bit(output));
QWARN(qPrintable(errorMessage));
}
output = rccProcess.readAllStandardError();
if (!output.isEmpty()) {
QString errorMessage = QString::fromLatin1("rcc stderr: %1").arg(QString::fromLocal8Bit(output));
QWARN(qPrintable(errorMessage));
}
QDir::setCurrent(currentDir);
}
static QStringList readLinesFromFile(const QString &fileName)
{
QFile file(fileName);
bool ok = file.open(QIODevice::ReadOnly | QIODevice::Text);
if (!ok)
QWARN(qPrintable(QString::fromLatin1("Could not open testdata file %1: %2").arg(fileName, file.errorString())));
QStringList lines = QString::fromUtf8(file.readAll()).split(QLatin1Char('\n'), QString::SkipEmptyParts);
return lines;
}
static QStringMap readExpectedFiles(const QString &fileName)
{
QStringMap expectedFiles;
QStringList lines = readLinesFromFile(fileName);
foreach (const QString &line, lines) {
QString resourceFileName = line.section(QLatin1Char(' '), 0, 0, QString::SectionSkipEmpty);
QString actualFileName = line.section(QLatin1Char(' '), 1, 1, QString::SectionSkipEmpty);
expectedFiles[resourceFileName] = actualFileName;
}
return expectedFiles;
}
/*
The following test looks for all *.qrc files under data/binary/. For each
.qrc file found, these files are processed (assuming the file found is
called "base.qrc"):
- base.qrc : processed by rcc; creates base.rcc
- base.locale : (optional) list of locales to test, one per line
- base.expected : list of pairs (file path in resource, path to real file),
one per line; the pair separated by a whitespace; the paths to real files
relative to data/binary/ (for testing the C locale)
- base.localeName.expected : for each localeName in the base.locale file,
as the above .expected file
*/
void tst_rcc::binary_data()
{
QTest::addColumn<QString>("resourceFile");
QTest::addColumn<QLocale>("locale");
QTest::addColumn<QString>("baseDirectory");
QTest::addColumn<QStringMap>("expectedFiles");
QString dataPath = QFINDTESTDATA("data/binary/");
if (dataPath.isEmpty())
QFAIL("data path not found");
QDirIterator iter(dataPath, QStringList() << QLatin1String("*.qrc"));
while (iter.hasNext())
{
iter.next();
QFileInfo qrcFileInfo = iter.fileInfo();
QString absoluteBaseName = QFileInfo(qrcFileInfo.absolutePath(), qrcFileInfo.baseName()).absoluteFilePath();
QString rccFileName = absoluteBaseName + QLatin1String(".rcc");
createRccBinaryData(m_rcc, dataPath, qrcFileInfo.absoluteFilePath(), rccFileName);
QString localeFileName = absoluteBaseName + QLatin1String(".locale");
QFile localeFile(localeFileName);
if (localeFile.exists()) {
QStringList locales = readLinesFromFile(localeFileName);
foreach (const QString &locale, locales) {
QString expectedFileName = QString::fromLatin1("%1.%2.%3").arg(absoluteBaseName, locale, QLatin1String("expected"));
QStringMap expectedFiles = readExpectedFiles(expectedFileName);
QTest::newRow(qPrintable(qrcFileInfo.baseName() + QLatin1Char('_') + locale)) << rccFileName
<< QLocale(locale)
<< dataPath
<< expectedFiles;
}
}
// always test for the C locale as well
QString expectedFileName = absoluteBaseName + QLatin1String(".expected");
QStringMap expectedFiles = readExpectedFiles(expectedFileName);
QTest::newRow(qPrintable(qrcFileInfo.baseName() + QLatin1String("_C"))) << rccFileName
<< QLocale::c()
<< dataPath
<< expectedFiles;
}
}
void tst_rcc::binary()
{
QFETCH(QString, baseDirectory);
QFETCH(QString, resourceFile);
QFETCH(QLocale, locale);
QFETCH(QStringMap, expectedFiles);
const QString rootPrefix = QLatin1String("/test_root/");
const QString resourceRootPrefix = QLatin1Char(':') + rootPrefix;
QLocale oldDefaultLocale;
QLocale::setDefault(locale);
QVERIFY(QFile::exists(resourceFile));
QVERIFY(QResource::registerResource(resourceFile, rootPrefix));
{ // need to destroy the iterators on the resource, in order to be able to unregister it
// read all the files inside the resources
QDirIterator iter(resourceRootPrefix, QDir::Files, QDirIterator::Subdirectories);
QList<QString> filesFound;
while (iter.hasNext())
filesFound << iter.next();
// add the test root prefix to the expected file names
QList<QString> expectedFileNames = expectedFiles.keys();
for (QList<QString>::iterator i = expectedFileNames.begin(); i < expectedFileNames.end(); ++i) {
// poor man's canonicalPath, which doesn't work with resources
if ((*i).startsWith(QLatin1Char('/')))
(*i).remove(0, 1);
*i = resourceRootPrefix + *i;
}
// check that we have all (and only) the expected files
std::sort(filesFound.begin(), filesFound.end());
std::sort(expectedFileNames.begin(), expectedFileNames.end());
QCOMPARE(filesFound, expectedFileNames);
// now actually check the file contents
QDir directory(baseDirectory);
for (QStringMap::const_iterator i = expectedFiles.constBegin(); i != expectedFiles.constEnd(); ++i) {
QString resourceFileName = i.key();
QString actualFileName = i.value();
QFile resourceFile(resourceRootPrefix + resourceFileName);
QVERIFY(resourceFile.open(QIODevice::ReadOnly));
QByteArray resourceData = resourceFile.readAll();
resourceFile.close();
QFile actualFile(QFileInfo(directory, actualFileName).absoluteFilePath());
QVERIFY(actualFile.open(QIODevice::ReadOnly));
QByteArray actualData = actualFile.readAll();
actualFile.close();
QCOMPARE(resourceData, actualData);
}
}
QVERIFY(QResource::unregisterResource(resourceFile, rootPrefix));
QLocale::setDefault(oldDefaultLocale);
}
void tst_rcc::readback_data()
{
QTest::addColumn<QString>("resourceName");
QTest::addColumn<QString>("fileSystemName");
QTest::newRow("data-0") << ":data/data-0.txt" << "sizes/data/data-0.txt";
QTest::newRow("data-1") << ":data/data-1.txt" << "sizes/data/data-1.txt";
QTest::newRow("data-2") << ":data/data-2.txt" << "sizes/data/data-2.txt";
QTest::newRow("data-35") << ":data/data-35.txt" << "sizes/data/data-35.txt";
QTest::newRow("circle") << ":images/circle.png" << "images/images/circle.png";
QTest::newRow("square") << ":images/square.png" << "images/images/square.png";
QTest::newRow("triangle") << ":images/subdir/triangle.png"
<< "images/images/subdir/triangle.png";
}
void tst_rcc::readback()
{
QFETCH(QString, resourceName);
QFETCH(QString, fileSystemName);
QString dataPath = QFINDTESTDATA("data/");
if (dataPath.isEmpty())
QFAIL("data path not found");
QFile resourceFile(resourceName);
QVERIFY(resourceFile.open(QIODevice::ReadOnly));
QByteArray resourceData = resourceFile.readAll();
resourceFile.close();
QFile fileSystemFile(dataPath + fileSystemName);
QVERIFY(fileSystemFile.open(QIODevice::ReadOnly));
QByteArray fileSystemData = fileSystemFile.readAll();
fileSystemFile.close();
QCOMPARE(resourceData, fileSystemData);
}
void tst_rcc::cleanupTestCase()
{
QString dataPath = QFINDTESTDATA("data/binary/");
if (dataPath.isEmpty())
return;
QDir dataDir(dataPath);
QFileInfoList entries = dataDir.entryInfoList(QStringList() << QLatin1String("*.rcc"));
foreach (const QFileInfo &entry, entries)
QFile::remove(entry.absoluteFilePath());
}
QTEST_MAIN(tst_rcc)
#include "tst_rcc.moc"