1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25
26 #include "qrcparser.h"
27
28 #include <utils/qtcassert.h>
29
30 #include <QCoreApplication>
31 #include <QDir>
32 #include <QDomDocument>
33 #include <QFile>
34 #include <QFileInfo>
35 #include <QLocale>
36 #include <QLoggingCategory>
37 #include <QMultiHash>
38 #include <QMutex>
39 #include <QMutexLocker>
40 #include <QSet>
41 #include <QStringList>
42
43 static Q_LOGGING_CATEGORY(qrcParserLog, "qtc.qrcParser", QtWarningMsg)
44
45 namespace Utils {
46
47 namespace Internal {
48
49 class QrcParserPrivate
50 {
51 Q_DECLARE_TR_FUNCTIONS(QmlJS::QrcParser)
52 public:
53 typedef QMap<QString,QStringList> SMap;
54 QrcParserPrivate(QrcParser *q);
55 bool parseFile(const QString &path, const QString &contents);
56 QString firstFileAtPath(const QString &path, const QLocale &locale) const;
57 void collectFilesAtPath(const QString &path, QStringList *res, const QLocale *locale = nullptr) const;
58 bool hasDirAtPath(const QString &path, const QLocale *locale = nullptr) const;
59 void collectFilesInPath(const QString &path, QMap<QString,QStringList> *res, bool addDirs = false,
60 const QLocale *locale = nullptr) const;
61 void collectResourceFilesForSourceFile(const QString &sourceFile, QStringList *res,
62 const QLocale *locale = nullptr) const;
63
64 QStringList errorMessages() const;
65 QStringList languages() const;
66 private:
67 static QString fixPrefix(const QString &prefix);
68 const QStringList allUiLanguages(const QLocale *locale) const;
69
70 SMap m_resources;
71 SMap m_files;
72 QStringList m_languages;
73 QStringList m_errorMessages;
74 };
75
76 class QrcCachePrivate
77 {
78 Q_DECLARE_TR_FUNCTIONS(QmlJS::QrcCachePrivate)
79 public:
80 QrcCachePrivate(QrcCache *q);
81 QrcParser::Ptr addPath(const QString &path, const QString &contents);
82 void removePath(const QString &path);
83 QrcParser::Ptr updatePath(const QString &path, const QString &contents);
84 QrcParser::Ptr parsedPath(const QString &path);
85 void clear();
86 private:
87 QHash<QString, QPair<QrcParser::Ptr,int> > m_cache;
88 QMutex m_mutex;
89 };
90 } // namespace Internal
91
92 /*!
93 \class Utils::QrcParser
94 \inmodule QtCreator
95 \brief The QrcParser class parses one or more QRC files and keeps their
96 content cached.
97
98 A \l{The Qt Resource System}{Qt resource collection (QRC)} contains files
99 read from the file system but organized in a possibly different way.
100 To easily describe that with a simple structure, we use a map from QRC paths
101 to the paths in the filesystem.
102 By using a map, we can easily find all QRC paths that start with a given
103 prefix, and thus loop on a QRC directory.
104
105 QRC files also support languages, which are mapped to a prefix of the QRC
106 path. For example, the French /image/bla.png (lang=fr) will have the path
107 \c {fr/image/bla.png}. The empty language represents the default resource.
108 Languages are looked up using the locale uiLanguages() property
109
110 For a single QRC, a given path maps to a single file, but when one has
111 multiple (platform-specific and mutually exclusive) QRC files, multiple
112 files match, so QStringList are used.
113
114 Especially, the \c collect* functions are thought of as low level interface.
115 */
116
117 /*!
118 \typedef QrcParser::Ptr
119 Represents pointers.
120 */
121
122 /*!
123 \typedef QrcParser::ConstPtr
124 Represents constant pointers.
125 */
126
127 /*!
128 Normalizes the \a path to a file in a QRC resource by dropping the \c qrc:/
129 or \c : and any extra slashes in the beginning.
130 */
normalizedQrcFilePath(const QString & path)131 QString QrcParser::normalizedQrcFilePath(const QString &path) {
132 QString normPath = path;
133 int endPrefix = 0;
134 if (path.startsWith(QLatin1String("qrc:/")))
135 endPrefix = 4;
136 else if (path.startsWith(QLatin1String(":/")))
137 endPrefix = 1;
138 if (endPrefix < path.size() && path.at(endPrefix) == QLatin1Char('/'))
139 while (endPrefix + 1 < path.size() && path.at(endPrefix+1) == QLatin1Char('/'))
140 ++endPrefix;
141 normPath = path.right(path.size()-endPrefix);
142 if (!normPath.startsWith(QLatin1Char('/')))
143 normPath.insert(0, QLatin1Char('/'));
144 return normPath;
145 }
146
147 /*!
148 Returns the path to a directory normalized to \a path in a QRC resource by
149 dropping the \c qrc:/ or \c : and any extra slashes at the beginning, and
150 by ensuring that the path ends with a slash
151 */
normalizedQrcDirectoryPath(const QString & path)152 QString QrcParser::normalizedQrcDirectoryPath(const QString &path) {
153 QString normPath = normalizedQrcFilePath(path);
154 if (!normPath.endsWith(QLatin1Char('/')))
155 normPath.append(QLatin1Char('/'));
156 return normPath;
157 }
158
159 /*!
160 Returns the QRC directory path for \a file.
161 */
qrcDirectoryPathForQrcFilePath(const QString & file)162 QString QrcParser::qrcDirectoryPathForQrcFilePath(const QString &file)
163 {
164 return file.left(file.lastIndexOf(QLatin1Char('/')));
165 }
166
QrcParser()167 QrcParser::QrcParser()
168 {
169 d = new Internal::QrcParserPrivate(this);
170 }
171
172 /*!
173 \internal
174 */
~QrcParser()175 QrcParser::~QrcParser()
176 {
177 delete d;
178 }
179
180 /*!
181 Parses the QRC file at \a path. If \a contents is not empty, it is used as
182 the file contents instead of reading it from the file system.
183
184 Returns whether the parsing succeeded.
185
186 \sa errorMessages(), parseQrcFile()
187 */
parseFile(const QString & path,const QString & contents)188 bool QrcParser::parseFile(const QString &path, const QString &contents)
189 {
190 return d->parseFile(path, contents);
191 }
192
193 /*!
194 Returns the file system path of the first (active) file at the given QRC
195 \a path and \a locale.
196 */
firstFileAtPath(const QString & path,const QLocale & locale) const197 QString QrcParser::firstFileAtPath(const QString &path, const QLocale &locale) const
198 {
199 return d->firstFileAtPath(path, locale);
200 }
201
202 /*!
203 Adds the file system paths for the given QRC \a path to \a res.
204
205 If \a locale is null, all possible files are added. Otherwise, just
206 the first one that matches the locale is added.
207 */
collectFilesAtPath(const QString & path,QStringList * res,const QLocale * locale) const208 void QrcParser::collectFilesAtPath(const QString &path, QStringList *res, const QLocale *locale) const
209 {
210 d->collectFilesAtPath(path, res, locale);
211 }
212
213 /*!
214 Returns \c true if \a path is a non-empty directory and matches \a locale.
215
216 */
hasDirAtPath(const QString & path,const QLocale * locale) const217 bool QrcParser::hasDirAtPath(const QString &path, const QLocale *locale) const
218 {
219 return d->hasDirAtPath(path, locale);
220 }
221
222 /*!
223 Adds the directory contents of the given QRC \a path to \a res if \a addDirs
224 is set to \c true.
225
226 Adds the QRC filename to file system path associations contained in the
227 given \a path to \a res. If addDirs() is \c true, directories are also
228 added.
229
230 If \a locale is null, all possible files are added. Otherwise, just the
231 first file with a matching the locale is added.
232 */
collectFilesInPath(const QString & path,QMap<QString,QStringList> * res,bool addDirs,const QLocale * locale) const233 void QrcParser::collectFilesInPath(const QString &path, QMap<QString,QStringList> *res, bool addDirs,
234 const QLocale *locale) const
235 {
236 d->collectFilesInPath(path, res, addDirs, locale);
237 }
238
239 /*!
240 Adds the resource files from the QRC file \a sourceFile to \a res.
241
242 If \a locale is null, all possible files are added. Otherwise, just
243 the first file with a matching the locale is added.
244 */
collectResourceFilesForSourceFile(const QString & sourceFile,QStringList * res,const QLocale * locale) const245 void QrcParser::collectResourceFilesForSourceFile(const QString &sourceFile, QStringList *res,
246 const QLocale *locale) const
247 {
248 d->collectResourceFilesForSourceFile(sourceFile, res, locale);
249 }
250
251 /*!
252 Returns the errors found while parsing.
253 */
errorMessages() const254 QStringList QrcParser::errorMessages() const
255 {
256 return d->errorMessages();
257 }
258
259 /*!
260 Returns all languages used in this QRC.
261 */
languages() const262 QStringList QrcParser::languages() const
263 {
264 return d->languages();
265 }
266
267 /*!
268 Indicates whether the QRC contents are valid.
269
270 Returns an error if the QRC is empty.
271 */
isValid() const272 bool QrcParser::isValid() const
273 {
274 return errorMessages().isEmpty();
275 }
276
277 /*!
278 Returns the \a contents of the QRC file at \a path.
279 */
parseQrcFile(const QString & path,const QString & contents)280 QrcParser::Ptr QrcParser::parseQrcFile(const QString &path, const QString &contents)
281 {
282 Ptr res(new QrcParser);
283 if (!path.isEmpty())
284 res->parseFile(path, contents);
285 return res;
286 }
287
288 // ----------------
289
290 /*!
291 \class Utils::QrcCache
292 \inmodule QtCreator
293 \brief The QrcCache class caches the contents of parsed QRC files.
294
295 \sa Utils::QrcParser
296 */
297
QrcCache()298 QrcCache::QrcCache()
299 {
300 d = new Internal::QrcCachePrivate(this);
301 }
302
303 /*!
304 \internal
305 */
~QrcCache()306 QrcCache::~QrcCache()
307 {
308 delete d;
309 }
310
311 /*!
312 Parses the QRC file at \a path and caches the parser. If \a contents is not
313 empty, it is used as the file contents instead of reading it from the file
314 system.
315
316 Returns whether the parsing succeeded.
317
318 \sa QrcParser::errorMessages(), QrcParser::parseQrcFile()
319 */
addPath(const QString & path,const QString & contents)320 QrcParser::ConstPtr QrcCache::addPath(const QString &path, const QString &contents)
321 {
322 return d->addPath(path, contents);
323 }
324
325 /*!
326 Removes \a path from the cache.
327 */
removePath(const QString & path)328 void QrcCache::removePath(const QString &path)
329 {
330 d->removePath(path);
331 }
332
333 /*!
334 Reparses the QRC file at \a path and returns the \a contents of the file.
335 */
updatePath(const QString & path,const QString & contents)336 QrcParser::ConstPtr QrcCache::updatePath(const QString &path, const QString &contents)
337 {
338 return d->updatePath(path, contents);
339 }
340
341 /*!
342 Returns the cached QRC parser for the QRC file at \a path.
343 */
parsedPath(const QString & path)344 QrcParser::ConstPtr QrcCache::parsedPath(const QString &path)
345 {
346 return d->parsedPath(path);
347 }
348
349 /*!
350 Clears the contents of the cache.
351 */
clear()352 void QrcCache::clear()
353 {
354 d->clear();
355 }
356
357 // --------------------
358
359 namespace Internal {
360
QrcParserPrivate(QrcParser *)361 QrcParserPrivate::QrcParserPrivate(QrcParser *)
362 { }
363
parseFile(const QString & path,const QString & contents)364 bool QrcParserPrivate::parseFile(const QString &path, const QString &contents)
365 {
366 QDomDocument doc;
367 QDir baseDir(QFileInfo(path).path());
368
369 if (contents.isEmpty()) {
370 // Regular file
371 QFile file(path);
372 if (!file.open(QIODevice::ReadOnly)) {
373 m_errorMessages.append(file.errorString());
374 return false;
375 }
376
377 QString error_msg;
378 int error_line, error_col;
379 if (!doc.setContent(&file, &error_msg, &error_line, &error_col)) {
380 m_errorMessages.append(tr("XML error on line %1, col %2: %3")
381 .arg(error_line).arg(error_col).arg(error_msg));
382 return false;
383 }
384 } else {
385 // Virtual file from qmake evaluator
386 QString error_msg;
387 int error_line, error_col;
388 if (!doc.setContent(contents, &error_msg, &error_line, &error_col)) {
389 m_errorMessages.append(tr("XML error on line %1, col %2: %3")
390 .arg(error_line).arg(error_col).arg(error_msg));
391 return false;
392 }
393 }
394
395 QDomElement root = doc.firstChildElement(QLatin1String("RCC"));
396 if (root.isNull()) {
397 m_errorMessages.append(tr("The <RCC> root element is missing."));
398 return false;
399 }
400
401 QDomElement relt = root.firstChildElement(QLatin1String("qresource"));
402 for (; !relt.isNull(); relt = relt.nextSiblingElement(QLatin1String("qresource"))) {
403
404 QString prefix = fixPrefix(relt.attribute(QLatin1String("prefix")));
405 const QString language = relt.attribute(QLatin1String("lang"));
406 if (!m_languages.contains(language))
407 m_languages.append(language);
408
409 QDomElement felt = relt.firstChildElement(QLatin1String("file"));
410 for (; !felt.isNull(); felt = felt.nextSiblingElement(QLatin1String("file"))) {
411 const QString fileName = felt.text();
412 const QString alias = felt.attribute(QLatin1String("alias"));
413 QString filePath = baseDir.absoluteFilePath(fileName);
414 QString accessPath;
415 if (!alias.isEmpty())
416 accessPath = language + prefix + alias;
417 else
418 accessPath = language + prefix + fileName;
419 QStringList &resources = m_resources[accessPath];
420 if (!resources.contains(filePath))
421 resources.append(filePath);
422 QStringList &files = m_files[filePath];
423 if (!files.contains(accessPath))
424 files.append(accessPath);
425 }
426 }
427 return true;
428 }
429
430 // path is assumed to be a normalized absolute path
firstFileAtPath(const QString & path,const QLocale & locale) const431 QString QrcParserPrivate::firstFileAtPath(const QString &path, const QLocale &locale) const
432 {
433 QTC_CHECK(path.startsWith(QLatin1Char('/')));
434 for (const QString &language : allUiLanguages(&locale)) {
435 if (m_languages.contains(language)) {
436 SMap::const_iterator res = m_resources.find(language + path);
437 if (res != m_resources.end())
438 return res.value().at(0);
439 }
440 }
441 return QString();
442 }
443
collectFilesAtPath(const QString & path,QStringList * files,const QLocale * locale) const444 void QrcParserPrivate::collectFilesAtPath(const QString &path, QStringList *files,
445 const QLocale *locale) const
446 {
447 QTC_CHECK(path.startsWith(QLatin1Char('/')));
448 for (const QString &language : allUiLanguages(locale)) {
449 if (m_languages.contains(language)) {
450 SMap::const_iterator res = m_resources.find(language + path);
451 if (res != m_resources.end())
452 (*files) << res.value();
453 }
454 }
455 }
456
457 // path is expected to be normalized and start and end with a slash
hasDirAtPath(const QString & path,const QLocale * locale) const458 bool QrcParserPrivate::hasDirAtPath(const QString &path, const QLocale *locale) const
459 {
460 QTC_CHECK(path.startsWith(QLatin1Char('/')));
461 QTC_CHECK(path.endsWith(QLatin1Char('/')));
462 for (const QString &language : allUiLanguages(locale)) {
463 if (m_languages.contains(language)) {
464 QString key = language + path;
465 SMap::const_iterator res = m_resources.lowerBound(key);
466 if (res != m_resources.end() && res.key().startsWith(key))
467 return true;
468 }
469 }
470 return false;
471 }
472
collectFilesInPath(const QString & path,QMap<QString,QStringList> * contents,bool addDirs,const QLocale * locale) const473 void QrcParserPrivate::collectFilesInPath(const QString &path, QMap<QString,QStringList> *contents,
474 bool addDirs, const QLocale *locale) const
475 {
476 QTC_CHECK(path.startsWith(QLatin1Char('/')));
477 QTC_CHECK(path.endsWith(QLatin1Char('/')));
478 SMap::const_iterator end = m_resources.end();
479 for (const QString &language : allUiLanguages(locale)) {
480 QString key = language + path;
481 SMap::const_iterator res = m_resources.lowerBound(key);
482 while (res != end && res.key().startsWith(key)) {
483 const QString &actualKey = res.key();
484 int endDir = actualKey.indexOf(QLatin1Char('/'), key.size());
485 if (endDir == -1) {
486 QString fileName = res.key().right(res.key().size()-key.size());
487 QStringList &els = (*contents)[fileName];
488 for (const QString &val : res.value())
489 if (!els.contains(val))
490 els << val;
491 ++res;
492 } else {
493 QString dirName = res.key().mid(key.size(), endDir - key.size() + 1);
494 if (addDirs)
495 contents->insert(dirName, QStringList());
496 QString key2 = key + dirName;
497 do {
498 ++res;
499 } while (res != end && res.key().startsWith(key2));
500 }
501 }
502 }
503 }
504
collectResourceFilesForSourceFile(const QString & sourceFile,QStringList * results,const QLocale * locale) const505 void QrcParserPrivate::collectResourceFilesForSourceFile(const QString &sourceFile,
506 QStringList *results,
507 const QLocale *locale) const
508 {
509 // TODO: use FileName from fileutils for file paths
510
511 const QStringList langs = allUiLanguages(locale);
512 SMap::const_iterator file = m_files.find(sourceFile);
513 if (file == m_files.end())
514 return;
515 for (const QString &resource : file.value()) {
516 for (const QString &language : langs) {
517 if (resource.startsWith(language) && !results->contains(resource))
518 results->append(resource);
519 }
520 }
521 }
522
errorMessages() const523 QStringList QrcParserPrivate::errorMessages() const
524 {
525 return m_errorMessages;
526 }
527
languages() const528 QStringList QrcParserPrivate::languages() const
529 {
530 return m_languages;
531 }
532
fixPrefix(const QString & prefix)533 QString QrcParserPrivate::fixPrefix(const QString &prefix)
534 {
535 const QChar slash = QLatin1Char('/');
536 QString result = QString(slash);
537 for (int i = 0; i < prefix.size(); ++i) {
538 const QChar c = prefix.at(i);
539 if (c == slash && result.at(result.size() - 1) == slash)
540 continue;
541 result.append(c);
542 }
543
544 if (!result.endsWith(slash))
545 result.append(slash);
546
547 return result;
548 }
549
allUiLanguages(const QLocale * locale) const550 const QStringList QrcParserPrivate::allUiLanguages(const QLocale *locale) const
551 {
552 if (!locale)
553 return languages();
554 bool hasEmptyString = false;
555 const QStringList langs = locale->uiLanguages();
556 QStringList allLangs = langs;
557 for (const QString &language : langs) {
558 if (language.isEmpty())
559 hasEmptyString = true;
560 else if (language.contains('_') || language.contains('-')) {
561 const QStringList splits = QString(language).replace('_', '-').split('-');
562 if (splits.size() > 1 && !allLangs.contains(splits.at(0)))
563 allLangs.append(splits.at(0));
564 }
565 }
566 if (!hasEmptyString)
567 allLangs.append(QString());
568 return allLangs;
569 }
570
571 // ----------------
572
QrcCachePrivate(QrcCache *)573 QrcCachePrivate::QrcCachePrivate(QrcCache *)
574 { }
575
addPath(const QString & path,const QString & contents)576 QrcParser::Ptr QrcCachePrivate::addPath(const QString &path, const QString &contents)
577 {
578 QPair<QrcParser::Ptr,int> currentValue;
579 {
580 QMutexLocker l(&m_mutex);
581 currentValue = m_cache.value(path, {QrcParser::Ptr(nullptr), 0});
582 currentValue.second += 1;
583 if (currentValue.second > 1) {
584 m_cache.insert(path, currentValue);
585 return currentValue.first;
586 }
587 }
588 QrcParser::Ptr newParser = QrcParser::parseQrcFile(path, contents);
589 if (!newParser->isValid())
590 qCWarning(qrcParserLog) << "adding invalid qrc " << path << " to the cache:" << newParser->errorMessages();
591 {
592 QMutexLocker l(&m_mutex);
593 QPair<QrcParser::Ptr,int> currentValue = m_cache.value(path, {QrcParser::Ptr(nullptr), 0});
594 if (currentValue.first.isNull())
595 currentValue.first = newParser;
596 currentValue.second += 1;
597 m_cache.insert(path, currentValue);
598 return currentValue.first;
599 }
600 }
601
removePath(const QString & path)602 void QrcCachePrivate::removePath(const QString &path)
603 {
604 QPair<QrcParser::Ptr,int> currentValue;
605 {
606 QMutexLocker l(&m_mutex);
607 currentValue = m_cache.value(path, {QrcParser::Ptr(nullptr), 0});
608 if (currentValue.second == 1) {
609 m_cache.remove(path);
610 } else if (currentValue.second > 1) {
611 currentValue.second -= 1;
612 m_cache.insert(path, currentValue);
613 } else {
614 QTC_CHECK(!m_cache.contains(path));
615 }
616 }
617 }
618
updatePath(const QString & path,const QString & contents)619 QrcParser::Ptr QrcCachePrivate::updatePath(const QString &path, const QString &contents)
620 {
621 QrcParser::Ptr newParser = QrcParser::parseQrcFile(path, contents);
622 {
623 QMutexLocker l(&m_mutex);
624 QPair<QrcParser::Ptr,int> currentValue = m_cache.value(path, {QrcParser::Ptr(nullptr), 0});
625 currentValue.first = newParser;
626 if (currentValue.second == 0)
627 currentValue.second = 1; // add qrc files that are not in the resources of a project
628 m_cache.insert(path, currentValue);
629 return currentValue.first;
630 }
631 }
632
parsedPath(const QString & path)633 QrcParser::Ptr QrcCachePrivate::parsedPath(const QString &path)
634 {
635 QMutexLocker l(&m_mutex);
636 QPair<QrcParser::Ptr,int> currentValue = m_cache.value(path, {QrcParser::Ptr(nullptr), 0});
637 return currentValue.first;
638 }
639
clear()640 void QrcCachePrivate::clear()
641 {
642 QMutexLocker l(&m_mutex);
643 m_cache.clear();
644 }
645
646 } // namespace Internal
647 } // namespace QmlJS
648