1 /****************************************************************************
2 **
3 ** Copyright (C) 2018 The Qt Company Ltd.
4 ** Copyright (C) 2018 Intel Corporation.
5 ** Contact: https://www.qt.io/licensing/
6 **
7 ** This file is part of the tools applications of the Qt Toolkit.
8 **
9 ** $QT_BEGIN_LICENSE:GPL-EXCEPT$
10 ** Commercial License Usage
11 ** Licensees holding valid commercial Qt licenses may use this file in
12 ** accordance with the commercial license agreement provided with the
13 ** Software or, alternatively, in accordance with the terms contained in
14 ** a written agreement between you and The Qt Company. For licensing terms
15 ** and conditions see https://www.qt.io/terms-conditions. For further
16 ** information use the contact form at https://www.qt.io/contact-us.
17 **
18 ** GNU General Public License Usage
19 ** Alternatively, this file may be used under the terms of the GNU
20 ** General Public License version 3 as published by the Free Software
21 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
22 ** included in the packaging of this file. Please review the following
23 ** information to ensure the GNU General Public License requirements will
24 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
25 **
26 ** $QT_END_LICENSE$
27 **
28 ****************************************************************************/
29 
30 #include <rcc.h>
31 
32 #include <qdebug.h>
33 #include <qdir.h>
34 #include <qfile.h>
35 #include <qfileinfo.h>
36 #include <qhashfunctions.h>
37 #include <qtextstream.h>
38 #include <qatomic.h>
39 #include <qglobal.h>
40 #include <qcoreapplication.h>
41 #include <qcommandlineoption.h>
42 #include <qcommandlineparser.h>
43 
44 #ifdef Q_OS_WIN
45 #  include <fcntl.h>
46 #  include <io.h>
47 #  include <stdio.h>
48 #endif // Q_OS_WIN
49 
50 QT_BEGIN_NAMESPACE
51 
dumpRecursive(const QDir & dir,QTextStream & out)52 void dumpRecursive(const QDir &dir, QTextStream &out)
53 {
54     const QFileInfoList entries = dir.entryInfoList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot
55                                                     | QDir::NoSymLinks);
56     for (const QFileInfo &entry : entries) {
57         if (entry.isDir()) {
58             dumpRecursive(entry.filePath(), out);
59         } else {
60             out << QLatin1String("<file>")
61                 << entry.filePath()
62                 << QLatin1String("</file>\n");
63         }
64     }
65 }
66 
createProject(const QString & outFileName)67 int createProject(const QString &outFileName)
68 {
69     QDir currentDir = QDir::current();
70     QString currentDirName = currentDir.dirName();
71     if (currentDirName.isEmpty())
72         currentDirName = QLatin1String("root");
73 
74     QFile file;
75     bool isOk = false;
76     if (outFileName.isEmpty()) {
77         isOk = file.open(stdout, QFile::WriteOnly | QFile::Text);
78     } else {
79         file.setFileName(outFileName);
80         isOk = file.open(QFile::WriteOnly | QFile::Text);
81     }
82     if (!isOk) {
83         fprintf(stderr, "Unable to open %s: %s\n",
84                 outFileName.isEmpty() ? qPrintable(outFileName) : "standard output",
85                 qPrintable(file.errorString()));
86         return 1;
87     }
88 
89     QTextStream out(&file);
90     out << QLatin1String("<!DOCTYPE RCC><RCC version=\"1.0\">\n"
91                          "<qresource>\n");
92 
93     // use "." as dir to get relative file pathes
94     dumpRecursive(QDir(QLatin1String(".")), out);
95 
96     out << QLatin1String("</qresource>\n"
97                          "</RCC>\n");
98 
99     return 0;
100 }
101 
102 // Escapes a path for use in a Depfile (Makefile syntax)
makefileEscape(const QString & filepath)103 QString makefileEscape(const QString &filepath)
104 {
105     // Always use forward slashes
106     QString result = QDir::cleanPath(filepath);
107     // Spaces are escaped with a backslash
108     result.replace(QLatin1Char(' '), QLatin1String("\\ "));
109     // Pipes are escaped with a backslash
110     result.replace(QLatin1Char('|'), QLatin1String("\\|"));
111     // Dollars are escaped with a dollar
112     result.replace(QLatin1Char('$'), QLatin1String("$$"));
113 
114     return result;
115 }
116 
writeDepFile(QIODevice & iodev,const QStringList & depsList,const QString & targetName)117 void writeDepFile(QIODevice &iodev, const QStringList &depsList, const QString &targetName)
118 {
119     QTextStream out(&iodev);
120     out << qPrintable(makefileEscape(targetName));
121     out << QLatin1Char(':');
122 
123     // Write depfile
124     for (int i = 0; i < depsList.size(); ++i) {
125         out << QLatin1Char(' ');
126 
127         out << qPrintable(makefileEscape(depsList.at(i)));
128     }
129 
130     out << QLatin1Char('\n');
131 }
132 
runRcc(int argc,char * argv[])133 int runRcc(int argc, char *argv[])
134 {
135     QCoreApplication app(argc, argv);
136     QCoreApplication::setApplicationVersion(QStringLiteral(QT_VERSION_STR));
137 
138     // Note that rcc isn't translated.
139     // If you use this code as an example for a translated app, make sure to translate the strings.
140     QCommandLineParser parser;
141     parser.setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions);
142     parser.setApplicationDescription(QLatin1String("Qt Resource Compiler version " QT_VERSION_STR));
143     parser.addHelpOption();
144     parser.addVersionOption();
145 
146     QCommandLineOption outputOption(QStringList() << QStringLiteral("o") << QStringLiteral("output"));
147     outputOption.setDescription(QStringLiteral("Write output to <file> rather than stdout."));
148     outputOption.setValueName(QStringLiteral("file"));
149     parser.addOption(outputOption);
150 
151     QCommandLineOption tempOption(QStringList() << QStringLiteral("t") << QStringLiteral("temp"));
152     tempOption.setDescription(QStringLiteral("Use temporary <file> for big resources."));
153     tempOption.setValueName(QStringLiteral("file"));
154     parser.addOption(tempOption);
155 
156     QCommandLineOption nameOption(QStringLiteral("name"), QStringLiteral("Create an external initialization function with <name>."), QStringLiteral("name"));
157     parser.addOption(nameOption);
158 
159     QCommandLineOption rootOption(QStringLiteral("root"), QStringLiteral("Prefix resource access path with root path."), QStringLiteral("path"));
160     parser.addOption(rootOption);
161 
162 #if QT_CONFIG(zstd) && !defined(QT_NO_COMPRESS)
163 #  define ALGOS     "[zstd], zlib, none"
164 #elif QT_CONFIG(zstd)
165 #  define ALGOS     "[zstd], none"
166 #elif !defined(QT_NO_COMPRESS)
167 #  define ALGOS     "[zlib], none"
168 #else
169 #  define ALGOS     "[none]"
170 #endif
171     const QString &algoDescription =
172             QStringLiteral("Compress input files using algorithm <algo> (" ALGOS ").");
173     QCommandLineOption compressionAlgoOption(QStringLiteral("compress-algo"), algoDescription, QStringLiteral("algo"));
174     parser.addOption(compressionAlgoOption);
175 #undef ALGOS
176 
177     QCommandLineOption compressOption(QStringLiteral("compress"), QStringLiteral("Compress input files by <level>."), QStringLiteral("level"));
178     parser.addOption(compressOption);
179 
180     QCommandLineOption nocompressOption(QStringLiteral("no-compress"), QStringLiteral("Disable all compression. Same as --compress-algo=none."));
181     parser.addOption(nocompressOption);
182 
183     QCommandLineOption thresholdOption(QStringLiteral("threshold"), QStringLiteral("Threshold to consider compressing files."), QStringLiteral("level"));
184     parser.addOption(thresholdOption);
185 
186     QCommandLineOption binaryOption(QStringLiteral("binary"), QStringLiteral("Output a binary file for use as a dynamic resource."));
187     parser.addOption(binaryOption);
188 
189     QCommandLineOption generatorOption(QStringList{QStringLiteral("g"), QStringLiteral("generator")});
190     generatorOption.setDescription(QStringLiteral("Select generator."));
191     generatorOption.setValueName(QStringLiteral("cpp|python|python2"));
192     parser.addOption(generatorOption);
193 
194     QCommandLineOption passOption(QStringLiteral("pass"), QStringLiteral("Pass number for big resources"), QStringLiteral("number"));
195     parser.addOption(passOption);
196 
197     QCommandLineOption namespaceOption(QStringLiteral("namespace"), QStringLiteral("Turn off namespace macros."));
198     parser.addOption(namespaceOption);
199 
200     QCommandLineOption verboseOption(QStringLiteral("verbose"), QStringLiteral("Enable verbose mode."));
201     parser.addOption(verboseOption);
202 
203     QCommandLineOption listOption(QStringLiteral("list"), QStringLiteral("Only list .qrc file entries, do not generate code."));
204     parser.addOption(listOption);
205 
206     QCommandLineOption mapOption(QStringLiteral("list-mapping"),
207                                  QStringLiteral("Only output a mapping of resource paths to file system paths defined in the .qrc file, do not generate code."));
208     parser.addOption(mapOption);
209 
210     QCommandLineOption depFileOption(QStringList{QStringLiteral("d"), QStringLiteral("depfile")},
211                                      QStringLiteral("Write a depfile with the .qrc dependencies to <file>."), QStringLiteral("file"));
212     parser.addOption(depFileOption);
213 
214     QCommandLineOption projectOption(QStringLiteral("project"), QStringLiteral("Output a resource file containing all files from the current directory."));
215     parser.addOption(projectOption);
216 
217     QCommandLineOption formatVersionOption(QStringLiteral("format-version"), QStringLiteral("The RCC format version to write"), QStringLiteral("number"));
218     parser.addOption(formatVersionOption);
219 
220     parser.addPositionalArgument(QStringLiteral("inputs"), QStringLiteral("Input files (*.qrc)."));
221 
222 
223     //parse options
224     parser.process(app);
225 
226     QString errorMsg;
227 
228     quint8 formatVersion = 3;
229     if (parser.isSet(formatVersionOption)) {
230         bool ok = false;
231         formatVersion = parser.value(formatVersionOption).toUInt(&ok);
232         if (!ok) {
233             errorMsg = QLatin1String("Invalid format version specified");
234         } else if (formatVersion < 1 || formatVersion > 3) {
235             errorMsg = QLatin1String("Unsupported format version specified");
236         }
237     }
238 
239     RCCResourceLibrary library(formatVersion);
240     if (parser.isSet(nameOption))
241         library.setInitName(parser.value(nameOption));
242     if (parser.isSet(rootOption)) {
243         library.setResourceRoot(QDir::cleanPath(parser.value(rootOption)));
244         if (library.resourceRoot().isEmpty()
245                 || library.resourceRoot().at(0) != QLatin1Char('/'))
246             errorMsg = QLatin1String("Root must start with a /");
247     }
248 
249     if (parser.isSet(compressionAlgoOption))
250         library.setCompressionAlgorithm(RCCResourceLibrary::parseCompressionAlgorithm(parser.value(compressionAlgoOption), &errorMsg));
251     if (formatVersion < 3 && library.compressionAlgorithm() == RCCResourceLibrary::CompressionAlgorithm::Zstd)
252         errorMsg = QLatin1String("Zstandard compression requires format version 3 or higher");
253     if (parser.isSet(nocompressOption))
254         library.setCompressionAlgorithm(RCCResourceLibrary::CompressionAlgorithm::None);
255     if (parser.isSet(compressOption) && errorMsg.isEmpty()) {
256         int level = library.parseCompressionLevel(library.compressionAlgorithm(), parser.value(compressOption), &errorMsg);
257         library.setCompressLevel(level);
258     }
259     if (parser.isSet(thresholdOption))
260         library.setCompressThreshold(parser.value(thresholdOption).toInt());
261     if (parser.isSet(binaryOption))
262         library.setFormat(RCCResourceLibrary::Binary);
263     if (parser.isSet(generatorOption)) {
264         auto value = parser.value(generatorOption);
265         if (value == QLatin1String("cpp"))
266             library.setFormat(RCCResourceLibrary::C_Code);
267         else if (value == QLatin1String("python"))
268             library.setFormat(RCCResourceLibrary::Python3_Code);
269         else if (value == QLatin1String("python2"))
270             library.setFormat(RCCResourceLibrary::Python2_Code);
271         else
272             errorMsg = QLatin1String("Invalid generator: ") + value;
273     }
274 
275     if (parser.isSet(passOption)) {
276         if (parser.value(passOption) == QLatin1String("1"))
277             library.setFormat(RCCResourceLibrary::Pass1);
278         else if (parser.value(passOption) == QLatin1String("2"))
279             library.setFormat(RCCResourceLibrary::Pass2);
280         else
281             errorMsg = QLatin1String("Pass number must be 1 or 2");
282     }
283     if (parser.isSet(namespaceOption))
284         library.setUseNameSpace(!library.useNameSpace());
285     if (parser.isSet(verboseOption))
286         library.setVerbose(true);
287 
288     const bool list = parser.isSet(listOption);
289     const bool map = parser.isSet(mapOption);
290     const bool projectRequested = parser.isSet(projectOption);
291     const QStringList filenamesIn = parser.positionalArguments();
292 
293     for (const QString &file : filenamesIn) {
294         if (file == QLatin1String("-"))
295             continue;
296         else if (!QFile::exists(file)) {
297             qWarning("%s: File does not exist '%s'", argv[0], qPrintable(file));
298             return 1;
299         }
300     }
301 
302     QString outFilename = parser.value(outputOption);
303     QString tempFilename = parser.value(tempOption);
304     QString depFilename = parser.value(depFileOption);
305 
306     if (projectRequested) {
307         return createProject(outFilename);
308     }
309 
310     if (filenamesIn.isEmpty())
311         errorMsg = QStringLiteral("No input files specified.");
312 
313     if (!errorMsg.isEmpty()) {
314         fprintf(stderr, "%s: %s\n", argv[0], qPrintable(errorMsg));
315         parser.showHelp(1);
316         return 1;
317     }
318     QFile errorDevice;
319     errorDevice.open(stderr, QIODevice::WriteOnly|QIODevice::Text);
320 
321     if (library.verbose())
322         errorDevice.write("Qt resource compiler\n");
323 
324     library.setInputFiles(filenamesIn);
325 
326     if (!library.readFiles(list || map, errorDevice))
327         return 1;
328 
329     QFile out;
330 
331     // open output
332     QIODevice::OpenMode mode = QIODevice::NotOpen;
333     switch (library.format()) {
334         case RCCResourceLibrary::C_Code:
335         case RCCResourceLibrary::Pass1:
336         case RCCResourceLibrary::Python3_Code:
337         case RCCResourceLibrary::Python2_Code:
338             mode = QIODevice::WriteOnly | QIODevice::Text;
339             break;
340         case RCCResourceLibrary::Pass2:
341         case RCCResourceLibrary::Binary:
342             mode = QIODevice::WriteOnly;
343             break;
344     }
345 
346 
347     if (outFilename.isEmpty() || outFilename == QLatin1String("-")) {
348 #ifdef Q_OS_WIN
349         // Make sure fwrite to stdout doesn't do LF->CRLF
350         if (library.format() == RCCResourceLibrary::Binary)
351             _setmode(_fileno(stdout), _O_BINARY);
352         // Make sure QIODevice does not do LF->CRLF,
353         // otherwise we'll end up in CRCRLF instead of
354         // CRLF.
355         mode &= ~QIODevice::Text;
356 #endif // Q_OS_WIN
357         // using this overload close() only flushes.
358         out.open(stdout, mode);
359     } else {
360         out.setFileName(outFilename);
361         if (!out.open(mode)) {
362             const QString msg = QString::fromLatin1("Unable to open %1 for writing: %2\n")
363                                 .arg(outFilename, out.errorString());
364             errorDevice.write(msg.toUtf8());
365             return 1;
366         }
367     }
368 
369     // do the task
370     if (list) {
371         const QStringList data = library.dataFiles();
372         for (int i = 0; i < data.size(); ++i) {
373             out.write(qPrintable(QDir::cleanPath(data.at(i))));
374             out.write("\n");
375         }
376         return 0;
377     }
378 
379     if (map) {
380         const RCCResourceLibrary::ResourceDataFileMap data = library.resourceDataFileMap();
381         for (auto it = data.begin(), end = data.end(); it != end; ++it) {
382             out.write(qPrintable(it.key()));
383             out.write("\t");
384             out.write(qPrintable(QDir::cleanPath(it.value())));
385             out.write("\n");
386         }
387         return 0;
388     }
389 
390     // Write depfile
391     if (!depFilename.isEmpty()) {
392         QFile depout;
393         depout.setFileName(depFilename);
394 
395         if (outFilename.isEmpty() || outFilename == QLatin1String("-")) {
396             const QString msg = QString::fromUtf8("Unable to write depfile when outputting to stdout!\n");
397             errorDevice.write(msg.toUtf8());
398             return 1;
399         }
400 
401         if (!depout.open(QIODevice::WriteOnly | QIODevice::Text)) {
402             const QString msg = QString::fromUtf8("Unable to open depfile %1 for writing: %2\n")
403                     .arg(depout.fileName(), depout.errorString());
404             errorDevice.write(msg.toUtf8());
405             return 1;
406         }
407 
408         writeDepFile(depout, library.dataFiles(), outFilename);
409         depout.close();
410     }
411 
412     QFile temp;
413     if (!tempFilename.isEmpty()) {
414         temp.setFileName(tempFilename);
415         if (!temp.open(QIODevice::ReadOnly)) {
416             const QString msg = QString::fromUtf8("Unable to open temporary file %1 for reading: %2\n")
417                     .arg(outFilename, out.errorString());
418             errorDevice.write(msg.toUtf8());
419             return 1;
420         }
421     }
422     bool success = library.output(out, temp, errorDevice);
423     if (!success) {
424         // erase the output file if we failed
425         out.remove();
426         return 1;
427     }
428     return 0;
429 }
430 
431 QT_END_NAMESPACE
432 
main(int argc,char * argv[])433 int main(int argc, char *argv[])
434 {
435     // rcc uses a QHash to store files in the resource system.
436     // we must force a certain hash order when testing or tst_rcc will fail, see QTBUG-25078
437     // similar requirements exist for reproducibly builds.
438     qSetGlobalQHashSeed(0);
439     if (qGlobalQHashSeed() != 0)
440         qWarning("Cannot force QHash seed");
441 
442     return QT_PREPEND_NAMESPACE(runRcc)(argc, argv);
443 }
444