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