BreezeIcons

qrcAlias.cpp
1/*
2 * SPDX-FileCopyrightText: 2016 Kåre Särs <kare.sars@iki.fi>
3 * SPDX-FileCopyrightText: 2024 Christoph Cullmann <cullmann@kde.org>
4 *
5 * SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7
8#include <QCommandLineParser>
9#include <QCoreApplication>
10#include <QCryptographicHash>
11#include <QDebug>
12#include <QDir>
13#include <QDirIterator>
14#include <QFile>
15#include <QFileInfo>
16#include <QHash>
17#include <QRegularExpression>
18#include <QSet>
19#include <QString>
20#include <QXmlStreamReader>
21
22void myMessageOutput(QtMsgType type, const QMessageLogContext &context, const QString &msg)
23{
24 QByteArray localMsg = msg.toLocal8Bit();
25 const char *file = context.file ? context.file : "";
26 const char *function = context.function ? context.function : "";
27 switch (type) {
28 case QtDebugMsg:
29 fprintf(stderr, "Debug: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
30 break;
31 case QtInfoMsg:
32 fprintf(stderr, "Info: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
33 break;
34 case QtWarningMsg:
35 fprintf(stderr, "Warning: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
36 break;
37 case QtCriticalMsg:
38 fprintf(stderr, "Critical: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
39 break;
40 case QtFatalMsg:
41 fprintf(stderr, "Fatal: %s (%s:%u, %s)\n", localMsg.constData(), file, context.line, function);
42 break;
43 }
44}
45
46/**
47 * Check if this file is a duplicate of an other on, dies then.
48 * @param fileName file to check
49 */
50static void checkForDuplicates(const QString &fileName)
51{
52 // get full content for dupe checking
53 QFile in(fileName);
54 if (!in.open(QIODevice::ReadOnly)) {
55 qFatal() << "failed to open" << in.fileName() << "for duplication checking";
56 }
57
58 // simplify content to catch files that just have spacing diffs
59 // that should not matter for SVGs
60 const auto fullContent = in.readAll().simplified();
62
63 hasher.addData(fullContent);
64 auto hash = hasher.result();
65
66 // see if we did have this content already and die
67 static QHash<QByteArray, QString> contentToFileName;
68 if (const auto it = contentToFileName.find(hash); it != contentToFileName.end()) {
69 qFatal() << "file" << fileName << "is a duplicate of file" << it.value();
70 }
71 contentToFileName.insert(hash, fileName);
72}
73
74/**
75 * Validate the XML, dies on errors.
76 * @param fileName file to validate
77 */
78static void validateXml(const QString &fileName)
79{
80 // read once and bail out on errors
81 QFile in(fileName);
82 if (!in.open(QIODevice::ReadOnly)) {
83 qFatal() << "failed to open" << in.fileName() << "for XML validation";
84 }
85 QXmlStreamReader xml(&in);
86 while (!xml.atEnd()) {
87 xml.readNext();
88 }
89 if (xml.hasError()) {
90 qFatal() << "XML error " << xml.errorString() << "in file" << in.fileName() << "at line" << xml.lineNumber();
91 }
92}
93
94/**
95 * Given a dir and a file inside, resolve the pseudo symlinks we get from Git on Windows.
96 * Does some consistency checks, will die if they fail.
97 *
98 * @param path directory that contains the given file
99 * @param fileName file name of the dir to check if it is a pseudo link
100 * @return target of the link or empty string if no link
101 */
102static QString resolveWindowsGitLink(const QString &path, const QString &fileName)
103{
104 QFile in(path + QLatin1Char('/') + fileName);
105 if (!in.open(QIODevice::ReadOnly)) {
106 qFatal() << "failed to open" << path << fileName << in.fileName();
107 }
108
109 QString firstLine = QString::fromLocal8Bit(in.readLine());
110 if (firstLine.isEmpty()) {
111 return QString();
112 }
113 QRegularExpression fNameReg(QStringLiteral("(.*\\.(?:svg|png|gif|ico))$"));
114 QRegularExpressionMatch match = fNameReg.match(firstLine);
115 if (!match.hasMatch()) {
116 return QString();
117 }
118
119 QFileInfo linkInfo(path + QLatin1Char('/') + match.captured(1));
120 QString aliasLink = resolveWindowsGitLink(linkInfo.path(), linkInfo.fileName());
121 if (!aliasLink.isEmpty()) {
122 // qDebug() << fileName << "=" << match.captured(1) << "=" << aliasLink;
123 return aliasLink;
124 }
125
126 return path + QLatin1Char('/') + match.captured(1);
127}
128
129/**
130 * Generates for the given directories a resource file with the full icon theme.
131 * Does some consistency checks, will die if they fail.
132 *
133 * @param indirs directories that contains the icons of the theme, the first is the versioned stuff,
134 * the remainings contain generated icons
135 * @param outfile QRC file to generate
136 */
137static void generateQRCAndCheckInputs(const QStringList &indirs, const QString &outfile)
138{
139 QFile out(outfile);
140 if (!out.open(QIODevice::WriteOnly)) {
141 qFatal() << "Failed to create" << outfile;
142 }
143 out.write("<!DOCTYPE RCC><RCC version=\"1.0\">\n");
144 out.write("<qresource>\n");
145
146 // loop over the inputs, remember if we do look at generated stuff for checks
147 bool generatedIcons = false;
148 QSet<QString> checkedFiles;
149 bool themeFileFound = false;
150 for (const auto &indir : indirs) {
151 // go to input dir to have proper relative paths
152 if (!QDir::setCurrent(indir)) {
153 qFatal() << "Failed to switch to input directory" << indir;
154 }
155
156 // we look at all interesting files in the indir and create a qrc with resolved symlinks
157 // we need QDir::System to get broken links for checking
158 QDirIterator it(QStringLiteral("."), {QStringLiteral("*.theme"), QStringLiteral("*.svg")}, QDir::Files | QDir::System, QDirIterator::Subdirectories);
159 while (it.hasNext()) {
160 // ensure nice path without ./ and Co.
161 const auto file = QDir::current().relativeFilePath(it.next());
162 const QFileInfo fileInfo(file);
163
164 // icons name shall not contain any kind of space
165 for (const auto &c : file) {
166 if (c.isSpace()) {
167 qFatal() << "Invalid file" << file << "with spaces in the name in input directory" << indir;
168 }
169 }
170
171 // per default we write the relative name as alias and the full path to pack in
172 // allows to generate the resource out of source, will already resolve normal symlinks
173 auto fullPath = fileInfo.canonicalFilePath();
174
175 // real symlink resolving for Unices, the rcc compiler ignores such files in -project mode
176 bool isLink = false;
177 if (fileInfo.isSymLink()) {
178 isLink = true;
179 }
180
181 // pseudo link files generated by Git on Windows
182 else if (const auto aliasLink = resolveWindowsGitLink(fileInfo.path(), fileInfo.fileName()); !aliasLink.isEmpty()) {
183 fullPath = QFileInfo(aliasLink).canonicalFilePath();
184 isLink = true;
185 }
186
187 // more checks for links
188 if (isLink) {
189 // empty canonical path means not found
190 if (fullPath.isEmpty()) {
191 qFatal() << "Broken symlink" << file << "in input directory" << indir;
192 }
193
194 // check that we don't link external stuff
195 if (!fullPath.startsWith(QFileInfo(indir).canonicalFilePath())) {
196 qFatal() << "Bad symlink" << file << "in input directory" << indir << "to external file" << fullPath;
197 }
198 }
199
200 // do some checks for SVGs
201 // do checks just once, if we encounter this multiple times because of aliasing
202 if (fullPath.endsWith(QLatin1String(".svg")) && !checkedFiles.contains(fullPath)) {
203 // fill our guard
204 checkedFiles.insert(fullPath);
205
206 // validate it as XML if it is an SVG
207 validateXml(fullPath);
208
209 // do duplicate check for non-generated icons
210 if (!generatedIcons) {
211 checkForDuplicates(fullPath);
212 }
213 } else if (fullPath.endsWith(QLatin1String(".theme"))) {
214 themeFileFound = true;
215 }
216
217 // write the one alias to file entry
218 out.write(QStringLiteral(" <file alias=\"%1\">%2</file>\n").arg(file, fullPath).toUtf8());
219 }
220
221 // starting with the second directory we look at generated icons
222 generatedIcons = true;
223 }
224
225 if (!themeFileFound) {
226 // without any theme file the icon theme will not work at runtime
227 qFatal() << "No theme file found!";
228 }
229
230 out.write("</qresource>\n");
231 out.write("</RCC>\n");
232}
233
234int main(int argc, char *argv[])
235{
236 qInstallMessageHandler(myMessageOutput);
237 QCoreApplication app(argc, argv);
238
239 QCommandLineParser parser;
240 QCommandLineOption outOption(QStringList() << QLatin1String("o") << QLatin1String("outfile"), QStringLiteral("Output qrc file"), QStringLiteral("outfile"));
241 parser.setApplicationDescription(QLatin1String("Create a resource file from the given input directories handling symlinks and pseudo symlink files."));
242 parser.addHelpOption();
243 parser.addVersionOption();
244 parser.addOption(outOption);
245 parser.process(app);
246
247 // do the generation and checks, will die on errors
248 generateQRCAndCheckInputs(parser.positionalArguments(), parser.value(outOption));
249 return 0;
250}
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
QString path(const QString &relativePath)
const char * constData() const const
QCommandLineOption addHelpOption()
bool addOption(const QCommandLineOption &option)
QCommandLineOption addVersionOption()
QStringList positionalArguments() const const
void process(const QCoreApplication &app)
void setApplicationDescription(const QString &description)
QString value(const QCommandLineOption &option) const const
bool addData(QIODevice *device)
QDir current()
QString relativeFilePath(const QString &fileName) const const
bool setCurrent(const QString &path)
QString canonicalFilePath() const const
iterator end()
iterator find(const Key &key)
iterator insert(const Key &key, const T &value)
bool contains(const QSet< T > &other) const const
iterator insert(const T &value)
QString fromLocal8Bit(QByteArrayView str)
bool isEmpty() const const
QByteArray toLocal8Bit() const const
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Mar 7 2025 11:57:51 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.