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 for (const auto &indir : indirs) {
121 // go to input dir to have proper relative paths
122 if (!QDir::setCurrent(indir)) {
123 qFatal() << "Failed to switch to input directory" << indir;
124 }
125
126 // we look at all interesting files in the indir and create a qrc with resolved symlinks
127 // we need QDir::System to get broken links for checking
128 QDirIterator it(QStringLiteral("."), {QStringLiteral("*.theme"), QStringLiteral("*.svg")}, QDir::Files | QDir::System, QDirIterator::Subdirectories);
129 while (it.hasNext()) {
130 // ensure nice path without ./ and Co.
131 const auto file = QDir::current().relativeFilePath(it.next());
132 const QFileInfo fileInfo(file);
133
134 // icons name shall not contain any kind of space
135 for (const auto &c : file) {
136 if (c.isSpace()) {
137 qFatal() << "Invalid file" << file << "with spaces in the name in input directory" << indir;
138 }
139 }
140
141 // per default we write the relative name as alias and the full path to pack in
142 // allows to generate the resource out of source, will already resolve normal symlinks
143 auto fullPath = fileInfo.canonicalFilePath();
144
145 // real symlink resolving for Unices, the rcc compiler ignores such files in -project mode
146 bool isLink = false;
147 if (fileInfo.isSymLink()) {
148 isLink = true;
149 }
150
151 // pseudo link files generated by Git on Windows
152 else if (const auto aliasLink = resolveWindowsGitLink(fileInfo.path(), fileInfo.fileName()); !aliasLink.isEmpty()) {
153 fullPath = QFileInfo(aliasLink).canonicalFilePath();
154 isLink = true;
155 }
156
157 // more checks for links
158 if (isLink) {
159 // empty canonical path means not found
160 if (fullPath.isEmpty()) {
161 qFatal() << "Broken symlink" << file << "in input directory" << indir;
162 }
163
164 // check that we don't link external stuff
165 if (!fullPath.startsWith(QFileInfo(indir).canonicalFilePath())) {
166 qFatal() << "Bad symlink" << file << "in input directory" << indir << "to external file" << fullPath;
167 }
168 }
169
170 // do some checks for SVGs
171 // do checks just once, if we encounter this multiple times because of aliasing
172 if (fullPath.endsWith(QLatin1String(".svg")) && !checkedFiles.contains(fullPath)) {
173 // fill our guard
174 checkedFiles.insert(fullPath);
175
176 // validate it as XML if it is an SVG
177 validateXml(fullPath);
178
179 // do duplicate check for non-generated icons
180 if (!generatedIcons) {
181 checkForDuplicates(fullPath);
182 }
183 }
184
185 // write the one alias to file entry
186 out.write(QStringLiteral(" <file alias=\"%1\">%2</file>\n").arg(file, fullPath).toUtf8());
187 }
188
189 // starting with the second directory we look at generated icons
190 generatedIcons = true;
191 }
192
193 out.write("</qresource>\n");
194 out.write("</RCC>\n");
195}
196
197int main(int argc, char *argv[])
198{
199 QCoreApplication app(argc, argv);
200
201 QCommandLineParser parser;
202 QCommandLineOption outOption(QStringList() << QLatin1String("o") << QLatin1String("outfile"), QStringLiteral("Output qrc file"), QStringLiteral("outfile"));
203 parser.setApplicationDescription(QLatin1String("Create a resource file from the given input directories handling symlinks and pseudo symlink files."));
204 parser.addHelpOption();
205 parser.addVersionOption();
206 parser.addOption(outOption);
207 parser.process(app);
208
209 // do the generation and checks, will die on errors
210 generateQRCAndCheckInputs(parser.positionalArguments(), parser.value(outOption));
211 return 0;
212}
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 Fri Oct 11 2024 12:16:18 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.