blob: 7185219d342003e0d0bca3077d6a9ab8bbc26578 [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2018 The Qt Company Ltd.
** Copyright (C) 2018 Intel Corporation.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the tools applications 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 "rcc.h"
#include <qbytearray.h>
#include <qdatetime.h>
#include <qdebug.h>
#include <qdir.h>
#include <qdiriterator.h>
#include <qfile.h>
#include <qiodevice.h>
#include <qlocale.h>
#include <qregexp.h>
#include <qstack.h>
#include <qxmlstream.h>
#include <algorithm>
#if QT_CONFIG(zstd)
# include <zstd.h>
#endif
// Note: A copy of this file is used in Qt Designer (qttools/src/designer/src/lib/shared/rcc.cpp)
QT_BEGIN_NAMESPACE
enum {
CONSTANT_USENAMESPACE = 1,
CONSTANT_COMPRESSLEVEL_DEFAULT = -1,
CONSTANT_ZSTDCOMPRESSLEVEL_CHECK = 1, // Zstd level to check if compressing is a good idea
CONSTANT_ZSTDCOMPRESSLEVEL_STORE = 14, // Zstd level to actually store the data
CONSTANT_COMPRESSTHRESHOLD_DEFAULT = 70
};
#if QT_CONFIG(zstd) && QT_VERSION >= QT_VERSION_CHECK(6,0,0)
# define CONSTANT_COMPRESSALGO_DEFAULT RCCResourceLibrary::CompressionAlgorithm::Zstd
#elif !defined(QT_NO_COMPRESS)
# define CONSTANT_COMPRESSALGO_DEFAULT RCCResourceLibrary::CompressionAlgorithm::Zlib
#else
# define CONSTANT_COMPRESSALGO_DEFAULT RCCResourceLibrary::CompressionAlgorithm::None
#endif
void RCCResourceLibrary::write(const char *str, int len)
{
int n = m_out.size();
m_out.resize(n + len);
memcpy(m_out.data() + n, str, len);
}
void RCCResourceLibrary::writeByteArray(const QByteArray &other)
{
if (m_format == Pass2) {
m_outDevice->write(other);
} else {
m_out.append(other);
}
}
static inline QString msgOpenReadFailed(const QString &fname, const QString &why)
{
return QString::fromLatin1("Unable to open %1 for reading: %2\n").arg(fname, why);
}
///////////////////////////////////////////////////////////
//
// RCCFileInfo
//
///////////////////////////////////////////////////////////
class RCCFileInfo
{
public:
enum Flags
{
// must match qresource.cpp
NoFlags = 0x00,
Compressed = 0x01,
Directory = 0x02,
CompressedZstd = 0x04
};
RCCFileInfo(const QString &name = QString(), const QFileInfo &fileInfo = QFileInfo(),
QLocale::Language language = QLocale::C,
QLocale::Country country = QLocale::AnyCountry,
uint flags = NoFlags,
RCCResourceLibrary::CompressionAlgorithm compressAlgo = CONSTANT_COMPRESSALGO_DEFAULT,
int compressLevel = CONSTANT_COMPRESSLEVEL_DEFAULT,
int compressThreshold = CONSTANT_COMPRESSTHRESHOLD_DEFAULT);
~RCCFileInfo();
QString resourceName() const;
public:
qint64 writeDataBlob(RCCResourceLibrary &lib, qint64 offset, QString *errorMessage);
qint64 writeDataName(RCCResourceLibrary &, qint64 offset);
void writeDataInfo(RCCResourceLibrary &lib);
int m_flags;
QString m_name;
QLocale::Language m_language;
QLocale::Country m_country;
QFileInfo m_fileInfo;
RCCFileInfo *m_parent;
QHash<QString, RCCFileInfo*> m_children;
RCCResourceLibrary::CompressionAlgorithm m_compressAlgo;
int m_compressLevel;
int m_compressThreshold;
qint64 m_nameOffset;
qint64 m_dataOffset;
qint64 m_childOffset;
};
RCCFileInfo::RCCFileInfo(const QString &name, const QFileInfo &fileInfo,
QLocale::Language language, QLocale::Country country, uint flags,
RCCResourceLibrary::CompressionAlgorithm compressAlgo, int compressLevel, int compressThreshold)
{
m_name = name;
m_fileInfo = fileInfo;
m_language = language;
m_country = country;
m_flags = flags;
m_parent = 0;
m_nameOffset = 0;
m_dataOffset = 0;
m_childOffset = 0;
m_compressAlgo = compressAlgo;
m_compressLevel = compressLevel;
m_compressThreshold = compressThreshold;
}
RCCFileInfo::~RCCFileInfo()
{
qDeleteAll(m_children);
}
QString RCCFileInfo::resourceName() const
{
QString resource = m_name;
for (RCCFileInfo *p = m_parent; p; p = p->m_parent)
resource = resource.prepend(p->m_name + QLatin1Char('/'));
return QLatin1Char(':') + resource;
}
void RCCFileInfo::writeDataInfo(RCCResourceLibrary &lib)
{
const bool text = lib.m_format == RCCResourceLibrary::C_Code;
const bool pass1 = lib.m_format == RCCResourceLibrary::Pass1;
const bool python = lib.m_format == RCCResourceLibrary::Python3_Code
|| lib.m_format == RCCResourceLibrary::Python2_Code;
//some info
if (text || pass1) {
if (m_language != QLocale::C) {
lib.writeString(" // ");
lib.writeByteArray(resourceName().toLocal8Bit());
lib.writeString(" [");
lib.writeByteArray(QByteArray::number(m_country));
lib.writeString("::");
lib.writeByteArray(QByteArray::number(m_language));
lib.writeString("[\n ");
} else {
lib.writeString(" // ");
lib.writeByteArray(resourceName().toLocal8Bit());
lib.writeString("\n ");
}
}
//pointer data
if (m_flags & RCCFileInfo::Directory) {
// name offset
lib.writeNumber4(m_nameOffset);
// flags
lib.writeNumber2(m_flags);
// child count
lib.writeNumber4(m_children.size());
// first child offset
lib.writeNumber4(m_childOffset);
} else {
// name offset
lib.writeNumber4(m_nameOffset);
// flags
lib.writeNumber2(m_flags);
// locale
lib.writeNumber2(m_country);
lib.writeNumber2(m_language);
//data offset
lib.writeNumber4(m_dataOffset);
}
if (text || pass1)
lib.writeChar('\n');
else if (python)
lib.writeString("\\\n");
if (lib.formatVersion() >= 2) {
// last modified time stamp
const QDateTime lastModified = m_fileInfo.lastModified();
quint64 lastmod = quint64(lastModified.isValid() ? lastModified.toMSecsSinceEpoch() : 0);
static const quint64 sourceDate = 1000 * qgetenv("QT_RCC_SOURCE_DATE_OVERRIDE").toULongLong();
if (sourceDate != 0)
lastmod = sourceDate;
static const quint64 sourceDate2 = 1000 * qgetenv("SOURCE_DATE_EPOCH").toULongLong();
if (sourceDate2 != 0)
lastmod = sourceDate2;
lib.writeNumber8(lastmod);
if (text || pass1)
lib.writeChar('\n');
else if (python)
lib.writeString("\\\n");
}
}
qint64 RCCFileInfo::writeDataBlob(RCCResourceLibrary &lib, qint64 offset,
QString *errorMessage)
{
const bool text = lib.m_format == RCCResourceLibrary::C_Code;
const bool pass1 = lib.m_format == RCCResourceLibrary::Pass1;
const bool pass2 = lib.m_format == RCCResourceLibrary::Pass2;
const bool binary = lib.m_format == RCCResourceLibrary::Binary;
const bool python = lib.m_format == RCCResourceLibrary::Python3_Code
|| lib.m_format == RCCResourceLibrary::Python2_Code;
//capture the offset
m_dataOffset = offset;
//find the data to be written
QFile file(m_fileInfo.absoluteFilePath());
if (!file.open(QFile::ReadOnly)) {
*errorMessage = msgOpenReadFailed(m_fileInfo.absoluteFilePath(), file.errorString());
return 0;
}
QByteArray data = file.readAll();
// Check if compression is useful for this file
if (data.size() != 0) {
#if QT_CONFIG(zstd)
if (m_compressAlgo == RCCResourceLibrary::CompressionAlgorithm::Best) {
m_compressAlgo = RCCResourceLibrary::CompressionAlgorithm::Zstd;
m_compressLevel = 19; // not ZSTD_maxCLevel(), as 20+ are experimental
}
if (m_compressAlgo == RCCResourceLibrary::CompressionAlgorithm::Zstd) {
if (lib.m_zstdCCtx == nullptr)
lib.m_zstdCCtx = ZSTD_createCCtx();
qsizetype size = data.size();
size = ZSTD_COMPRESSBOUND(size);
int compressLevel = m_compressLevel;
if (compressLevel < 0)
compressLevel = CONSTANT_ZSTDCOMPRESSLEVEL_CHECK;
QByteArray compressed(size, Qt::Uninitialized);
char *dst = const_cast<char *>(compressed.constData());
size_t n = ZSTD_compressCCtx(lib.m_zstdCCtx, dst, size,
data.constData(), data.size(),
compressLevel);
if (n * 100.0 < data.size() * 1.0 * (100 - m_compressThreshold) ) {
// compressing is worth it
if (m_compressLevel < 0) {
// heuristic compression, so recompress
n = ZSTD_compressCCtx(lib.m_zstdCCtx, dst, size,
data.constData(), data.size(),
CONSTANT_ZSTDCOMPRESSLEVEL_STORE);
}
if (ZSTD_isError(n)) {
QString msg = QString::fromLatin1("%1: error: compression with zstd failed: %2\n")
.arg(m_name, QString::fromUtf8(ZSTD_getErrorName(n)));
lib.m_errorDevice->write(msg.toUtf8());
} else if (lib.verbose()) {
QString msg = QString::fromLatin1("%1: note: compressed using zstd (%2 -> %3)\n")
.arg(m_name).arg(data.size()).arg(n);
lib.m_errorDevice->write(msg.toUtf8());
}
lib.m_overallFlags |= CompressedZstd;
m_flags |= CompressedZstd;
data = std::move(compressed);
data.truncate(n);
} else if (lib.verbose()) {
QString msg = QString::fromLatin1("%1: note: not compressed\n").arg(m_name);
lib.m_errorDevice->write(msg.toUtf8());
}
}
#endif
#ifndef QT_NO_COMPRESS
if (m_compressAlgo == RCCResourceLibrary::CompressionAlgorithm::Best) {
m_compressAlgo = RCCResourceLibrary::CompressionAlgorithm::Zlib;
m_compressLevel = 9;
}
if (m_compressAlgo == RCCResourceLibrary::CompressionAlgorithm::Zlib) {
QByteArray compressed =
qCompress(reinterpret_cast<uchar *>(data.data()), data.size(), m_compressLevel);
int compressRatio = int(100.0 * (data.size() - compressed.size()) / data.size());
if (compressRatio >= m_compressThreshold) {
if (lib.verbose()) {
QString msg = QString::fromLatin1("%1: note: compressed using zlib (%2 -> %3)\n")
.arg(m_name).arg(data.size()).arg(compressed.size());
lib.m_errorDevice->write(msg.toUtf8());
}
data = compressed;
lib.m_overallFlags |= Compressed;
m_flags |= Compressed;
} else if (lib.verbose()) {
QString msg = QString::fromLatin1("%1: note: not compressed\n").arg(m_name);
lib.m_errorDevice->write(msg.toUtf8());
}
}
#endif // QT_NO_COMPRESS
}
// some info
if (text || pass1) {
lib.writeString(" // ");
lib.writeByteArray(m_fileInfo.absoluteFilePath().toLocal8Bit());
lib.writeString("\n ");
}
// write the length
if (text || binary || pass2 || python)
lib.writeNumber4(data.size());
if (text || pass1)
lib.writeString("\n ");
else if (python)
lib.writeString("\\\n");
offset += 4;
// write the payload
const char *p = data.constData();
if (text || python) {
for (int i = data.size(), j = 0; --i >= 0; --j) {
lib.writeHex(*p++);
if (j == 0) {
if (text)
lib.writeString("\n ");
else
lib.writeString("\\\n");
j = 16;
}
}
} else if (binary || pass2) {
lib.writeByteArray(data);
}
offset += data.size();
// done
if (text || pass1)
lib.writeString("\n ");
else if (python)
lib.writeString("\\\n");
return offset;
}
qint64 RCCFileInfo::writeDataName(RCCResourceLibrary &lib, qint64 offset)
{
const bool text = lib.m_format == RCCResourceLibrary::C_Code;
const bool pass1 = lib.m_format == RCCResourceLibrary::Pass1;
const bool python = lib.m_format == RCCResourceLibrary::Python3_Code
|| lib.m_format == RCCResourceLibrary::Python2_Code;
// capture the offset
m_nameOffset = offset;
// some info
if (text || pass1) {
lib.writeString(" // ");
lib.writeByteArray(m_name.toLocal8Bit());
lib.writeString("\n ");
}
// write the length
lib.writeNumber2(m_name.length());
if (text || pass1)
lib.writeString("\n ");
else if (python)
lib.writeString("\\\n");
offset += 2;
// write the hash
lib.writeNumber4(qt_hash(m_name));
if (text || pass1)
lib.writeString("\n ");
else if (python)
lib.writeString("\\\n");
offset += 4;
// write the m_name
const QChar *unicode = m_name.unicode();
for (int i = 0; i < m_name.length(); ++i) {
lib.writeNumber2(unicode[i].unicode());
if ((text || pass1) && i % 16 == 0)
lib.writeString("\n ");
else if (python && i % 16 == 0)
lib.writeString("\\\n");
}
offset += m_name.length()*2;
// done
if (text || pass1)
lib.writeString("\n ");
else if (python)
lib.writeString("\\\n");
return offset;
}
///////////////////////////////////////////////////////////
//
// RCCResourceLibrary
//
///////////////////////////////////////////////////////////
RCCResourceLibrary::Strings::Strings() :
TAG_RCC(QLatin1String("RCC")),
TAG_RESOURCE(QLatin1String("qresource")),
TAG_FILE(QLatin1String("file")),
ATTRIBUTE_LANG(QLatin1String("lang")),
ATTRIBUTE_PREFIX(QLatin1String("prefix")),
ATTRIBUTE_ALIAS(QLatin1String("alias")),
ATTRIBUTE_THRESHOLD(QLatin1String("threshold")),
ATTRIBUTE_COMPRESS(QLatin1String("compress")),
ATTRIBUTE_COMPRESSALGO(QStringLiteral("compression-algorithm"))
{
}
RCCResourceLibrary::RCCResourceLibrary(quint8 formatVersion)
: m_root(0),
m_format(C_Code),
m_verbose(false),
m_compressionAlgo(CONSTANT_COMPRESSALGO_DEFAULT),
m_compressLevel(CONSTANT_COMPRESSLEVEL_DEFAULT),
m_compressThreshold(CONSTANT_COMPRESSTHRESHOLD_DEFAULT),
m_treeOffset(0),
m_namesOffset(0),
m_dataOffset(0),
m_overallFlags(0),
m_useNameSpace(CONSTANT_USENAMESPACE),
m_errorDevice(0),
m_outDevice(0),
m_formatVersion(formatVersion)
{
m_out.reserve(30 * 1000 * 1000);
#if QT_CONFIG(zstd)
m_zstdCCtx = nullptr;
#endif
}
RCCResourceLibrary::~RCCResourceLibrary()
{
delete m_root;
#if QT_CONFIG(zstd)
ZSTD_freeCCtx(m_zstdCCtx);
#endif
}
enum RCCXmlTag {
RccTag,
ResourceTag,
FileTag
};
Q_DECLARE_TYPEINFO(RCCXmlTag, Q_PRIMITIVE_TYPE);
bool RCCResourceLibrary::interpretResourceFile(QIODevice *inputDevice,
const QString &fname, QString currentPath, bool listMode)
{
Q_ASSERT(m_errorDevice);
const QChar slash = QLatin1Char('/');
if (!currentPath.isEmpty() && !currentPath.endsWith(slash))
currentPath += slash;
QXmlStreamReader reader(inputDevice);
QStack<RCCXmlTag> tokens;
QString prefix;
QLocale::Language language = QLocale::c().language();
QLocale::Country country = QLocale::c().country();
QString alias;
auto compressAlgo = m_compressionAlgo;
int compressLevel = m_compressLevel;
int compressThreshold = m_compressThreshold;
while (!reader.atEnd()) {
QXmlStreamReader::TokenType t = reader.readNext();
switch (t) {
case QXmlStreamReader::StartElement:
if (reader.name() == m_strings.TAG_RCC) {
if (!tokens.isEmpty())
reader.raiseError(QLatin1String("expected <RCC> tag"));
else
tokens.push(RccTag);
} else if (reader.name() == m_strings.TAG_RESOURCE) {
if (tokens.isEmpty() || tokens.top() != RccTag) {
reader.raiseError(QLatin1String("unexpected <RESOURCE> tag"));
} else {
tokens.push(ResourceTag);
QXmlStreamAttributes attributes = reader.attributes();
language = QLocale::c().language();
country = QLocale::c().country();
if (attributes.hasAttribute(m_strings.ATTRIBUTE_LANG)) {
QString attribute = attributes.value(m_strings.ATTRIBUTE_LANG).toString();
QLocale lang = QLocale(attribute);
language = lang.language();
if (2 == attribute.length()) {
// Language only
country = QLocale::AnyCountry;
} else {
country = lang.country();
}
}
prefix.clear();
if (attributes.hasAttribute(m_strings.ATTRIBUTE_PREFIX))
prefix = attributes.value(m_strings.ATTRIBUTE_PREFIX).toString();
if (!prefix.startsWith(slash))
prefix.prepend(slash);
if (!prefix.endsWith(slash))
prefix += slash;
}
} else if (reader.name() == m_strings.TAG_FILE) {
if (tokens.isEmpty() || tokens.top() != ResourceTag) {
reader.raiseError(QLatin1String("unexpected <FILE> tag"));
} else {
tokens.push(FileTag);
QXmlStreamAttributes attributes = reader.attributes();
alias.clear();
if (attributes.hasAttribute(m_strings.ATTRIBUTE_ALIAS))
alias = attributes.value(m_strings.ATTRIBUTE_ALIAS).toString();
compressAlgo = m_compressionAlgo;
compressLevel = m_compressLevel;
compressThreshold = m_compressThreshold;
QString errorString;
if (attributes.hasAttribute(m_strings.ATTRIBUTE_COMPRESSALGO))
compressAlgo = parseCompressionAlgorithm(attributes.value(m_strings.ATTRIBUTE_COMPRESSALGO), &errorString);
if (errorString.isEmpty() && attributes.hasAttribute(m_strings.ATTRIBUTE_COMPRESS)) {
QString value = attributes.value(m_strings.ATTRIBUTE_COMPRESS).toString();
compressLevel = parseCompressionLevel(compressAlgo, value, &errorString);
}
// Special case for -no-compress
if (m_compressLevel == -2)
compressAlgo = CompressionAlgorithm::None;
if (attributes.hasAttribute(m_strings.ATTRIBUTE_THRESHOLD))
compressThreshold = attributes.value(m_strings.ATTRIBUTE_THRESHOLD).toString().toInt();
if (!errorString.isEmpty())
reader.raiseError(errorString);
}
} else {
reader.raiseError(QString(QLatin1String("unexpected tag: %1")).arg(reader.name().toString()));
}
break;
case QXmlStreamReader::EndElement:
if (reader.name() == m_strings.TAG_RCC) {
if (!tokens.isEmpty() && tokens.top() == RccTag)
tokens.pop();
else
reader.raiseError(QLatin1String("unexpected closing tag"));
} else if (reader.name() == m_strings.TAG_RESOURCE) {
if (!tokens.isEmpty() && tokens.top() == ResourceTag)
tokens.pop();
else
reader.raiseError(QLatin1String("unexpected closing tag"));
} else if (reader.name() == m_strings.TAG_FILE) {
if (!tokens.isEmpty() && tokens.top() == FileTag)
tokens.pop();
else
reader.raiseError(QLatin1String("unexpected closing tag"));
}
break;
case QXmlStreamReader::Characters:
if (reader.isWhitespace())
break;
if (tokens.isEmpty() || tokens.top() != FileTag) {
reader.raiseError(QLatin1String("unexpected text"));
} else {
QString fileName = reader.text().toString();
if (fileName.isEmpty()) {
const QString msg = QString::fromLatin1("RCC: Warning: Null node in XML of '%1'\n").arg(fname);
m_errorDevice->write(msg.toUtf8());
}
if (alias.isNull())
alias = fileName;
alias = QDir::cleanPath(alias);
while (alias.startsWith(QLatin1String("../")))
alias.remove(0, 3);
alias = QDir::cleanPath(m_resourceRoot) + prefix + alias;
QString absFileName = fileName;
if (QDir::isRelativePath(absFileName))
absFileName.prepend(currentPath);
QFileInfo file(absFileName);
if (file.isDir()) {
QDir dir(file.filePath());
if (!alias.endsWith(slash))
alias += slash;
QDirIterator it(dir, QDirIterator::FollowSymlinks|QDirIterator::Subdirectories);
while (it.hasNext()) {
it.next();
QFileInfo child(it.fileInfo());
if (child.fileName() != QLatin1String(".") && child.fileName() != QLatin1String("..")) {
const bool arc =
addFile(alias + child.fileName(),
RCCFileInfo(child.fileName(),
child,
language,
country,
child.isDir() ? RCCFileInfo::Directory : RCCFileInfo::NoFlags,
compressAlgo,
compressLevel,
compressThreshold)
);
if (!arc)
m_failedResources.push_back(child.fileName());
}
}
} else if (listMode || file.isFile()) {
const bool arc =
addFile(alias,
RCCFileInfo(alias.section(slash, -1),
file,
language,
country,
RCCFileInfo::NoFlags,
compressAlgo,
compressLevel,
compressThreshold)
);
if (!arc)
m_failedResources.push_back(absFileName);
} else if (file.exists()) {
m_failedResources.push_back(absFileName);
const QString msg = QString::fromLatin1("RCC: Error in '%1': Entry '%2' is neither a file nor a directory\n")
.arg(fname, fileName);
m_errorDevice->write(msg.toUtf8());
return false;
} else {
m_failedResources.push_back(absFileName);
const QString msg = QString::fromLatin1("RCC: Error in '%1': Cannot find file '%2'\n")
.arg(fname, fileName);
m_errorDevice->write(msg.toUtf8());
return false;
}
}
break;
default:
break;
}
}
if (reader.hasError()) {
int errorLine = reader.lineNumber();
int errorColumn = reader.columnNumber();
QString errorMessage = reader.errorString();
QString msg = QString::fromLatin1("RCC Parse Error: '%1' Line: %2 Column: %3 [%4]\n").arg(fname).arg(errorLine).arg(errorColumn).arg(errorMessage);
m_errorDevice->write(msg.toUtf8());
return false;
}
if (m_root == 0) {
const QString msg = QString::fromLatin1("RCC: Warning: No resources in '%1'.\n").arg(fname);
m_errorDevice->write(msg.toUtf8());
if (!listMode && m_format == Binary) {
// create dummy entry, otherwise loading with QResource will crash
m_root = new RCCFileInfo(QString(), QFileInfo(),
QLocale::C, QLocale::AnyCountry, RCCFileInfo::Directory);
}
}
return true;
}
bool RCCResourceLibrary::addFile(const QString &alias, const RCCFileInfo &file)
{
Q_ASSERT(m_errorDevice);
if (file.m_fileInfo.size() > 0xffffffff) {
const QString msg = QString::fromLatin1("File too big: %1\n").arg(file.m_fileInfo.absoluteFilePath());
m_errorDevice->write(msg.toUtf8());
return false;
}
if (!m_root)
m_root = new RCCFileInfo(QString(), QFileInfo(), QLocale::C, QLocale::AnyCountry, RCCFileInfo::Directory);
RCCFileInfo *parent = m_root;
const QStringList nodes = alias.split(QLatin1Char('/'));
for (int i = 1; i < nodes.size()-1; ++i) {
const QString node = nodes.at(i);
if (node.isEmpty())
continue;
if (!parent->m_children.contains(node)) {
RCCFileInfo *s = new RCCFileInfo(node, QFileInfo(), QLocale::C, QLocale::AnyCountry, RCCFileInfo::Directory);
s->m_parent = parent;
parent->m_children.insert(node, s);
parent = s;
} else {
parent = parent->m_children[node];
}
}
const QString filename = nodes.at(nodes.size()-1);
RCCFileInfo *s = new RCCFileInfo(file);
s->m_parent = parent;
typedef QHash<QString, RCCFileInfo*>::const_iterator ChildConstIterator;
const ChildConstIterator cbegin = parent->m_children.constFind(filename);
const ChildConstIterator cend = parent->m_children.constEnd();
for (ChildConstIterator it = cbegin; it != cend; ++it) {
if (it.key() == filename && it.value()->m_language == s->m_language &&
it.value()->m_country == s->m_country) {
for (const QString &name : qAsConst(m_fileNames)) {
qWarning("%s: Warning: potential duplicate alias detected: '%s'",
qPrintable(name), qPrintable(filename));
}
break;
}
}
parent->m_children.insertMulti(filename, s);
return true;
}
void RCCResourceLibrary::reset()
{
if (m_root) {
delete m_root;
m_root = 0;
}
m_errorDevice = 0;
m_failedResources.clear();
}
bool RCCResourceLibrary::readFiles(bool listMode, QIODevice &errorDevice)
{
reset();
m_errorDevice = &errorDevice;
//read in data
if (m_verbose) {
const QString msg = QString::fromLatin1("Processing %1 files [listMode=%2]\n")
.arg(m_fileNames.size()).arg(static_cast<int>(listMode));
m_errorDevice->write(msg.toUtf8());
}
for (int i = 0; i < m_fileNames.size(); ++i) {
QFile fileIn;
QString fname = m_fileNames.at(i);
QString pwd;
if (fname == QLatin1String("-")) {
fname = QLatin1String("(stdin)");
pwd = QDir::currentPath();
fileIn.setFileName(fname);
if (!fileIn.open(stdin, QIODevice::ReadOnly)) {
m_errorDevice->write(msgOpenReadFailed(fname, fileIn.errorString()).toUtf8());
return false;
}
} else {
pwd = QFileInfo(fname).path();
fileIn.setFileName(fname);
if (!fileIn.open(QIODevice::ReadOnly)) {
m_errorDevice->write(msgOpenReadFailed(fname, fileIn.errorString()).toUtf8());
return false;
}
}
if (m_verbose) {
const QString msg = QString::fromLatin1("Interpreting %1\n").arg(fname);
m_errorDevice->write(msg.toUtf8());
}
if (!interpretResourceFile(&fileIn, fname, pwd, listMode))
return false;
}
return true;
}
QStringList RCCResourceLibrary::dataFiles() const
{
QStringList ret;
QStack<RCCFileInfo*> pending;
if (!m_root)
return ret;
pending.push(m_root);
while (!pending.isEmpty()) {
RCCFileInfo *file = pending.pop();
for (QHash<QString, RCCFileInfo*>::iterator it = file->m_children.begin();
it != file->m_children.end(); ++it) {
RCCFileInfo *child = it.value();
if (child->m_flags & RCCFileInfo::Directory)
pending.push(child);
else
ret.append(child->m_fileInfo.filePath());
}
}
return ret;
}
// Determine map of resource identifier (':/newPrefix/images/p1.png') to file via recursion
static void resourceDataFileMapRecursion(const RCCFileInfo *m_root, const QString &path, RCCResourceLibrary::ResourceDataFileMap &m)
{
typedef QHash<QString, RCCFileInfo*>::const_iterator ChildConstIterator;
const QChar slash = QLatin1Char('/');
const ChildConstIterator cend = m_root->m_children.constEnd();
for (ChildConstIterator it = m_root->m_children.constBegin(); it != cend; ++it) {
const RCCFileInfo *child = it.value();
const QString childName = path + slash + child->m_name;
if (child->m_flags & RCCFileInfo::Directory) {
resourceDataFileMapRecursion(child, childName, m);
} else {
m.insert(childName, child->m_fileInfo.filePath());
}
}
}
RCCResourceLibrary::ResourceDataFileMap RCCResourceLibrary::resourceDataFileMap() const
{
ResourceDataFileMap rc;
if (m_root)
resourceDataFileMapRecursion(m_root, QString(QLatin1Char(':')), rc);
return rc;
}
RCCResourceLibrary::CompressionAlgorithm RCCResourceLibrary::parseCompressionAlgorithm(QStringView value, QString *errorMsg)
{
if (value == QLatin1String("best"))
return CompressionAlgorithm::Best;
if (value == QLatin1String("zlib")) {
#ifdef QT_NO_COMPRESS
*errorMsg = QLatin1String("zlib support not compiled in");
#else
return CompressionAlgorithm::Zlib;
#endif
} else if (value == QLatin1String("zstd")) {
#if QT_CONFIG(zstd)
return CompressionAlgorithm::Zstd;
#else
*errorMsg = QLatin1String("Zstandard support not compiled in");
#endif
} else if (value != QLatin1String("none")) {
*errorMsg = QString::fromLatin1("Unknown compression algorithm '%1'").arg(value);
}
return CompressionAlgorithm::None;
}
int RCCResourceLibrary::parseCompressionLevel(CompressionAlgorithm algo, const QString &level, QString *errorMsg)
{
bool ok;
int c = level.toInt(&ok);
if (ok) {
switch (algo) {
case CompressionAlgorithm::None:
case CompressionAlgorithm::Best:
return 0;
case CompressionAlgorithm::Zlib:
if (c >= 1 && c <= 9)
return c;
break;
case CompressionAlgorithm::Zstd:
#if QT_CONFIG(zstd)
if (c >= 0 && c <= ZSTD_maxCLevel())
return c;
#endif
break;
}
}
*errorMsg = QString::fromLatin1("invalid compression level '%1'").arg(level);
return 0;
}
bool RCCResourceLibrary::output(QIODevice &outDevice, QIODevice &tempDevice, QIODevice &errorDevice)
{
m_errorDevice = &errorDevice;
if (m_format == Pass2) {
const char pattern[] = { 'Q', 'R', 'C', '_', 'D', 'A', 'T', 'A' };
bool foundSignature = false;
while (true) {
char c;
for (int i = 0; i < 8; ) {
if (!tempDevice.getChar(&c)) {
if (foundSignature)
return true;
m_errorDevice->write("No data signature found\n");
return false;
}
if (c == pattern[i]) {
++i;
} else {
for (int k = 0; k < i; ++k)
outDevice.putChar(pattern[k]);
outDevice.putChar(c);
i = 0;
}
}
m_outDevice = &outDevice;
quint64 start = outDevice.pos();
writeDataBlobs();
quint64 len = outDevice.pos() - start;
tempDevice.seek(tempDevice.pos() + len - 8);
foundSignature = true;
}
}
//write out
if (m_verbose)
m_errorDevice->write("Outputting code\n");
if (!writeHeader()) {
m_errorDevice->write("Could not write header\n");
return false;
}
if (m_root) {
if (!writeDataBlobs()) {
m_errorDevice->write("Could not write data blobs.\n");
return false;
}
if (!writeDataNames()) {
m_errorDevice->write("Could not write file names\n");
return false;
}
if (!writeDataStructure()) {
m_errorDevice->write("Could not write data tree\n");
return false;
}
}
if (!writeInitializer()) {
m_errorDevice->write("Could not write footer\n");
return false;
}
outDevice.write(m_out.constData(), m_out.size());
return true;
}
void RCCResourceLibrary::writeDecimal(int value)
{
Q_ASSERT(m_format != RCCResourceLibrary::Binary);
char buf[std::numeric_limits<int>::digits10 + 2];
int n = snprintf(buf, sizeof(buf), "%d", value);
write(buf, n);
}
static const char hexDigits[] = "0123456789abcdef";
inline void RCCResourceLibrary::write2HexDigits(quint8 number)
{
writeChar(hexDigits[number >> 4]);
writeChar(hexDigits[number & 0xf]);
}
void RCCResourceLibrary::writeHex(quint8 tmp)
{
switch (m_format) {
case RCCResourceLibrary::Python3_Code:
case RCCResourceLibrary::Python2_Code:
if (tmp >= 32 && tmp < 127 && tmp != '"' && tmp != '\\') {
writeChar(char(tmp));
} else {
writeChar('\\');
writeChar('x');
write2HexDigits(tmp);
}
break;
default:
writeChar('0');
writeChar('x');
if (tmp < 16)
writeChar(hexDigits[tmp]);
else
write2HexDigits(tmp);
writeChar(',');
break;
}
}
void RCCResourceLibrary::writeNumber2(quint16 number)
{
if (m_format == RCCResourceLibrary::Binary) {
writeChar(number >> 8);
writeChar(number);
} else {
writeHex(number >> 8);
writeHex(number);
}
}
void RCCResourceLibrary::writeNumber4(quint32 number)
{
if (m_format == RCCResourceLibrary::Pass2) {
m_outDevice->putChar(char(number >> 24));
m_outDevice->putChar(char(number >> 16));
m_outDevice->putChar(char(number >> 8));
m_outDevice->putChar(char(number));
} else if (m_format == RCCResourceLibrary::Binary) {
writeChar(number >> 24);
writeChar(number >> 16);
writeChar(number >> 8);
writeChar(number);
} else {
writeHex(number >> 24);
writeHex(number >> 16);
writeHex(number >> 8);
writeHex(number);
}
}
void RCCResourceLibrary::writeNumber8(quint64 number)
{
if (m_format == RCCResourceLibrary::Pass2) {
m_outDevice->putChar(char(number >> 56));
m_outDevice->putChar(char(number >> 48));
m_outDevice->putChar(char(number >> 40));
m_outDevice->putChar(char(number >> 32));
m_outDevice->putChar(char(number >> 24));
m_outDevice->putChar(char(number >> 16));
m_outDevice->putChar(char(number >> 8));
m_outDevice->putChar(char(number));
} else if (m_format == RCCResourceLibrary::Binary) {
writeChar(number >> 56);
writeChar(number >> 48);
writeChar(number >> 40);
writeChar(number >> 32);
writeChar(number >> 24);
writeChar(number >> 16);
writeChar(number >> 8);
writeChar(number);
} else {
writeHex(number >> 56);
writeHex(number >> 48);
writeHex(number >> 40);
writeHex(number >> 32);
writeHex(number >> 24);
writeHex(number >> 16);
writeHex(number >> 8);
writeHex(number);
}
}
bool RCCResourceLibrary::writeHeader()
{
switch (m_format) {
case C_Code:
case Pass1:
writeString("/****************************************************************************\n");
writeString("** Resource object code\n");
writeString("**\n");
writeString("** Created by: The Resource Compiler for Qt version ");
writeByteArray(QT_VERSION_STR);
writeString("\n**\n");
writeString("** WARNING! All changes made in this file will be lost!\n");
writeString( "*****************************************************************************/\n\n");
break;
case Python3_Code:
case Python2_Code:
writeString("# Resource object code (Python ");
writeChar(m_format == Python3_Code ? '3' : '2');
writeString(")\n");
writeString("# Created by: object code\n");
writeString("# Created by: The Resource Compiler for Qt version ");
writeByteArray(QT_VERSION_STR);
writeString("\n");
writeString("# WARNING! All changes made in this file will be lost!\n\n");
writeString("from PySide2 import QtCore\n\n");
break;
case Binary:
writeString("qres");
writeNumber4(0);
writeNumber4(0);
writeNumber4(0);
writeNumber4(0);
if (m_formatVersion >= 3)
writeNumber4(m_overallFlags);
break;
default:
break;
}
return true;
}
bool RCCResourceLibrary::writeDataBlobs()
{
Q_ASSERT(m_errorDevice);
switch (m_format) {
case C_Code:
writeString("static const unsigned char qt_resource_data[] = {\n");
break;
case Python3_Code:
writeString("qt_resource_data = b\"\\\n");
break;
case Python2_Code:
writeString("qt_resource_data = \"\\\n");
break;
case Binary:
m_dataOffset = m_out.size();
break;
default:
break;
}
if (!m_root)
return false;
QStack<RCCFileInfo*> pending;
pending.push(m_root);
qint64 offset = 0;
QString errorMessage;
while (!pending.isEmpty()) {
RCCFileInfo *file = pending.pop();
for (QHash<QString, RCCFileInfo*>::iterator it = file->m_children.begin();
it != file->m_children.end(); ++it) {
RCCFileInfo *child = it.value();
if (child->m_flags & RCCFileInfo::Directory)
pending.push(child);
else {
offset = child->writeDataBlob(*this, offset, &errorMessage);
if (offset == 0) {
m_errorDevice->write(errorMessage.toUtf8());
return false;
}
}
}
}
switch (m_format) {
case C_Code:
writeString("\n};\n\n");
break;
case Python3_Code:
case Python2_Code:
writeString("\"\n\n");
break;
case Pass1:
if (offset < 8)
offset = 8;
writeString("\nstatic const unsigned char qt_resource_data[");
writeByteArray(QByteArray::number(offset));
writeString("] = { 'Q', 'R', 'C', '_', 'D', 'A', 'T', 'A' };\n\n");
break;
default:
break;
}
return true;
}
bool RCCResourceLibrary::writeDataNames()
{
switch (m_format) {
case C_Code:
case Pass1:
writeString("static const unsigned char qt_resource_name[] = {\n");
break;
case Python3_Code:
writeString("qt_resource_name = b\"\\\n");
break;
case Python2_Code:
writeString("qt_resource_name = \"\\\n");
break;
case Binary:
m_namesOffset = m_out.size();
break;
default:
break;
}
QHash<QString, int> names;
QStack<RCCFileInfo*> pending;
if (!m_root)
return false;
pending.push(m_root);
qint64 offset = 0;
while (!pending.isEmpty()) {
RCCFileInfo *file = pending.pop();
for (QHash<QString, RCCFileInfo*>::iterator it = file->m_children.begin();
it != file->m_children.end(); ++it) {
RCCFileInfo *child = it.value();
if (child->m_flags & RCCFileInfo::Directory)
pending.push(child);
if (names.contains(child->m_name)) {
child->m_nameOffset = names.value(child->m_name);
} else {
names.insert(child->m_name, offset);
offset = child->writeDataName(*this, offset);
}
}
}
switch (m_format) {
case C_Code:
case Pass1:
writeString("\n};\n\n");
break;
case Python3_Code:
case Python2_Code:
writeString("\"\n\n");
break;
default:
break;
}
return true;
}
struct qt_rcc_compare_hash
{
typedef bool result_type;
result_type operator()(const RCCFileInfo *left, const RCCFileInfo *right) const
{
return qt_hash(left->m_name) < qt_hash(right->m_name);
}
};
bool RCCResourceLibrary::writeDataStructure()
{
switch (m_format) {
case C_Code:
case Pass1:
writeString("static const unsigned char qt_resource_struct[] = {\n");
break;
case Python3_Code:
writeString("qt_resource_struct = b\"\\\n");
break;
case Python2_Code:
writeString("qt_resource_struct = \"\\\n");
break;
case Binary:
m_treeOffset = m_out.size();
break;
default:
break;
}
QStack<RCCFileInfo*> pending;
if (!m_root)
return false;
//calculate the child offsets (flat)
pending.push(m_root);
int offset = 1;
while (!pending.isEmpty()) {
RCCFileInfo *file = pending.pop();
file->m_childOffset = offset;
//sort by hash value for binary lookup
QList<RCCFileInfo*> m_children = file->m_children.values();
std::sort(m_children.begin(), m_children.end(), qt_rcc_compare_hash());
//write out the actual data now
for (int i = 0; i < m_children.size(); ++i) {
RCCFileInfo *child = m_children.at(i);
++offset;
if (child->m_flags & RCCFileInfo::Directory)
pending.push(child);
}
}
//write out the structure (ie iterate again!)
pending.push(m_root);
m_root->writeDataInfo(*this);
while (!pending.isEmpty()) {
RCCFileInfo *file = pending.pop();
//sort by hash value for binary lookup
QList<RCCFileInfo*> m_children = file->m_children.values();
std::sort(m_children.begin(), m_children.end(), qt_rcc_compare_hash());
//write out the actual data now
for (int i = 0; i < m_children.size(); ++i) {
RCCFileInfo *child = m_children.at(i);
child->writeDataInfo(*this);
if (child->m_flags & RCCFileInfo::Directory)
pending.push(child);
}
}
switch (m_format) {
case C_Code:
case Pass1:
writeString("\n};\n\n");
break;
case Python3_Code:
case Python2_Code:
writeString("\"\n\n");
break;
default:
break;
}
return true;
}
void RCCResourceLibrary::writeMangleNamespaceFunction(const QByteArray &name)
{
if (m_useNameSpace) {
writeString("QT_RCC_MANGLE_NAMESPACE(");
writeByteArray(name);
writeChar(')');
} else {
writeByteArray(name);
}
}
void RCCResourceLibrary::writeAddNamespaceFunction(const QByteArray &name)
{
if (m_useNameSpace) {
writeString("QT_RCC_PREPEND_NAMESPACE(");
writeByteArray(name);
writeChar(')');
} else {
writeByteArray(name);
}
}
bool RCCResourceLibrary::writeInitializer()
{
if (m_format == C_Code || m_format == Pass1) {
//write("\nQT_BEGIN_NAMESPACE\n");
QString initNameStr = m_initName;
if (!initNameStr.isEmpty()) {
initNameStr.prepend(QLatin1Char('_'));
initNameStr.replace(QRegExp(QLatin1String("[^a-zA-Z0-9_]")), QLatin1String("_"));
}
QByteArray initName = initNameStr.toLatin1();
//init
if (m_useNameSpace) {
writeString("#ifdef QT_NAMESPACE\n"
"# define QT_RCC_PREPEND_NAMESPACE(name) ::QT_NAMESPACE::name\n"
"# define QT_RCC_MANGLE_NAMESPACE0(x) x\n"
"# define QT_RCC_MANGLE_NAMESPACE1(a, b) a##_##b\n"
"# define QT_RCC_MANGLE_NAMESPACE2(a, b) QT_RCC_MANGLE_NAMESPACE1(a,b)\n"
"# define QT_RCC_MANGLE_NAMESPACE(name) QT_RCC_MANGLE_NAMESPACE2( \\\n"
" QT_RCC_MANGLE_NAMESPACE0(name), QT_RCC_MANGLE_NAMESPACE0(QT_NAMESPACE))\n"
"#else\n"
"# define QT_RCC_PREPEND_NAMESPACE(name) name\n"
"# define QT_RCC_MANGLE_NAMESPACE(name) name\n"
"#endif\n\n");
writeString("#ifdef QT_NAMESPACE\n"
"namespace QT_NAMESPACE {\n"
"#endif\n\n");
}
if (m_root) {
writeString("bool qRegisterResourceData"
"(int, const unsigned char *, "
"const unsigned char *, const unsigned char *);\n");
writeString("bool qUnregisterResourceData"
"(int, const unsigned char *, "
"const unsigned char *, const unsigned char *);\n\n");
if (m_overallFlags & (RCCFileInfo::Compressed | RCCFileInfo::CompressedZstd)) {
// use variable relocations with ELF and Mach-O
writeString("#if defined(__ELF__) || defined(__APPLE__)\n");
if (m_overallFlags & RCCFileInfo::Compressed) {
writeString("static inline unsigned char qResourceFeatureZlib()\n"
"{\n"
" extern const unsigned char qt_resourceFeatureZlib;\n"
" return qt_resourceFeatureZlib;\n"
"}\n");
}
if (m_overallFlags & RCCFileInfo::CompressedZstd) {
writeString("static inline unsigned char qResourceFeatureZstd()\n"
"{\n"
" extern const unsigned char qt_resourceFeatureZstd;\n"
" return qt_resourceFeatureZstd;\n"
"}\n");
}
writeString("#else\n");
if (m_overallFlags & RCCFileInfo::Compressed)
writeString("unsigned char qResourceFeatureZlib();\n");
if (m_overallFlags & RCCFileInfo::CompressedZstd)
writeString("unsigned char qResourceFeatureZstd();\n");
writeString("#endif\n\n");
}
}
if (m_useNameSpace)
writeString("#ifdef QT_NAMESPACE\n}\n#endif\n\n");
QByteArray initResources = "qInitResources";
initResources += initName;
// Work around -Wmissing-declarations warnings.
writeString("int ");
writeMangleNamespaceFunction(initResources);
writeString("();\n");
writeString("int ");
writeMangleNamespaceFunction(initResources);
writeString("()\n{\n");
if (m_root) {
writeString(" int version = ");
writeDecimal(m_formatVersion);
writeString(";\n ");
writeAddNamespaceFunction("qRegisterResourceData");
writeString("\n (version, qt_resource_struct, "
"qt_resource_name, qt_resource_data);\n");
}
writeString(" return 1;\n");
writeString("}\n\n");
//cleanup
QByteArray cleanResources = "qCleanupResources";
cleanResources += initName;
// Work around -Wmissing-declarations warnings.
writeString("int ");
writeMangleNamespaceFunction(cleanResources);
writeString("();\n");
writeString("int ");
writeMangleNamespaceFunction(cleanResources);
writeString("()\n{\n");
if (m_root) {
writeString(" int version = ");
writeDecimal(m_formatVersion);
writeString(";\n ");
// ODR-use certain symbols from QtCore if we require optional features
if (m_overallFlags & RCCFileInfo::Compressed) {
writeString("version += ");
writeAddNamespaceFunction("qResourceFeatureZlib()");
writeString(";\n ");
}
if (m_overallFlags & RCCFileInfo::CompressedZstd) {
writeString("version += ");
writeAddNamespaceFunction("qResourceFeatureZstd()");
writeString(";\n ");
}
writeAddNamespaceFunction("qUnregisterResourceData");
writeString("\n (version, qt_resource_struct, "
"qt_resource_name, qt_resource_data);\n");
}
writeString(" return 1;\n");
writeString("}\n\n");
writeString("namespace {\n"
" struct initializer {\n");
if (m_useNameSpace) {
writeByteArray(" initializer() { QT_RCC_MANGLE_NAMESPACE(" + initResources + ")(); }\n"
" ~initializer() { QT_RCC_MANGLE_NAMESPACE(" + cleanResources + ")(); }\n");
} else {
writeByteArray(" initializer() { " + initResources + "(); }\n"
" ~initializer() { " + cleanResources + "(); }\n");
}
writeString(" } dummy;\n"
"}\n");
} else if (m_format == Binary) {
int i = 4;
char *p = m_out.data();
p[i++] = 0;
p[i++] = 0;
p[i++] = 0;
p[i++] = m_formatVersion;
p[i++] = (m_treeOffset >> 24) & 0xff;
p[i++] = (m_treeOffset >> 16) & 0xff;
p[i++] = (m_treeOffset >> 8) & 0xff;
p[i++] = (m_treeOffset >> 0) & 0xff;
p[i++] = (m_dataOffset >> 24) & 0xff;
p[i++] = (m_dataOffset >> 16) & 0xff;
p[i++] = (m_dataOffset >> 8) & 0xff;
p[i++] = (m_dataOffset >> 0) & 0xff;
p[i++] = (m_namesOffset >> 24) & 0xff;
p[i++] = (m_namesOffset >> 16) & 0xff;
p[i++] = (m_namesOffset >> 8) & 0xff;
p[i++] = (m_namesOffset >> 0) & 0xff;
if (m_formatVersion >= 3) {
p[i++] = (m_overallFlags >> 24) & 0xff;
p[i++] = (m_overallFlags >> 16) & 0xff;
p[i++] = (m_overallFlags >> 8) & 0xff;
p[i++] = (m_overallFlags >> 0) & 0xff;
}
} else if (m_format == Python3_Code || m_format == Python2_Code) {
writeString("def qInitResources():\n");
writeString(" QtCore.qRegisterResourceData(0x");
write2HexDigits(m_formatVersion);
writeString(", qt_resource_struct, qt_resource_name, qt_resource_data)\n\n");
writeString("def qCleanupResources():\n");
writeString(" QtCore.qUnregisterResourceData(0x");
write2HexDigits(m_formatVersion);
writeString(", qt_resource_struct, qt_resource_name, qt_resource_data)\n\n");
writeString("qInitResources()\n");
}
return true;
}
QT_END_NAMESPACE