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

KDE's Doxygen guidelines are available online.