1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the Qt Assistant of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU Lesser General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU Lesser
19** General Public License version 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPL3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or (at your option) the GNU General
28** Public license version 3 or any later version approved by the KDE Free
29** Qt Foundation. The licenses are as published by the Free Software
30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31** included in the packaging of this file. Please review the following
32** information to ensure the GNU General Public License requirements will
33** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34** https://www.gnu.org/licenses/gpl-3.0.html.
35**
36** $QT_END_LICENSE$
37**
38****************************************************************************/
39
40#include "qhelpprojectdata_p.h"
41
42#include <QtCore/QCoreApplication>
43#include <QtCore/QDir>
44#include <QtCore/QFileInfo>
45#include <QtCore/QStack>
46#include <QtCore/QMap>
47#include <QtCore/QRegExp>
48#include <QtCore/QTextStream>
49#include <QtCore/QUrl>
50#include <QtCore/QVariant>
51#include <QtCore/QXmlStreamReader>
52
53QT_BEGIN_NAMESPACE
54
55class QHelpProjectDataPrivate : public QXmlStreamReader
56{
57public:
58 void readData(const QByteArray &contents);
59
60 QString virtualFolder;
61 QString namespaceName;
62 QString fileName;
63 QString rootPath;
64
65 QList<QHelpDataCustomFilter> customFilterList;
66 QList<QHelpDataFilterSection> filterSectionList;
67 QMap<QString, QVariant> metaData;
68
69 QString errorMsg;
70
71private:
72 void readProject();
73 void readCustomFilter();
74 void readFilterSection();
75 void readTOC();
76 void readKeywords();
77 void readFiles();
78 void skipUnknownToken();
79 void addMatchingFiles(const QString &pattern);
80 bool hasValidSyntax(const QString &nameSpace, const QString &vFolder) const;
81
82 QMap<QString, QStringList> dirEntriesCache;
83};
84
85void QHelpProjectDataPrivate::skipUnknownToken()
86{
87 const QString message = QCoreApplication::translate(context: "QHelpProject",
88 key: "Skipping unknown token <%1> in file \"%2\".")
89 .arg(a: name()).arg(a: fileName) + QLatin1Char('\n');
90 fputs(qPrintable(message), stdout);
91
92 skipCurrentElement();
93}
94
95void QHelpProjectDataPrivate::readData(const QByteArray &contents)
96{
97 addData(data: contents);
98 while (!atEnd()) {
99 readNext();
100 if (isStartElement()) {
101 if (name() == QLatin1String("QtHelpProject")
102 && attributes().value(qualifiedName: QLatin1String("version"))
103 == QLatin1String("1.0")) {
104 readProject();
105 } else {
106 raiseError(message: QCoreApplication::translate(context: "QHelpProject",
107 key: "Unknown token. Expected \"QtHelpProject\"."));
108 }
109 }
110 }
111
112 if (hasError()) {
113 raiseError(message: QCoreApplication::translate(context: "QHelpProject",
114 key: "Error in line %1: %2").arg(a: lineNumber())
115 .arg(a: errorString()));
116 }
117}
118
119void QHelpProjectDataPrivate::readProject()
120{
121 while (!atEnd()) {
122 readNext();
123 if (isStartElement()) {
124 if (name() == QLatin1String("virtualFolder")) {
125 virtualFolder = readElementText();
126 if (!hasValidSyntax(nameSpace: QLatin1String("test"), vFolder: virtualFolder))
127 raiseError(message: QCoreApplication::translate(context: "QHelpProject",
128 key: "Virtual folder has invalid syntax in file: \"%1\"").arg(a: fileName));
129 } else if (name() == QLatin1String("namespace")) {
130 namespaceName = readElementText();
131 if (!hasValidSyntax(nameSpace: namespaceName, vFolder: QLatin1String("test")))
132 raiseError(message: QCoreApplication::translate(context: "QHelpProject",
133 key: "Namespace \"%1\" has invalid syntax in file: \"%2\"").arg(args&: namespaceName, args&: fileName));
134 } else if (name() == QLatin1String("customFilter")) {
135 readCustomFilter();
136 } else if (name() == QLatin1String("filterSection")) {
137 readFilterSection();
138 } else if (name() == QLatin1String("metaData")) {
139 QString n = attributes().value(qualifiedName: QLatin1String("name")).toString();
140 if (!metaData.contains(akey: n))
141 metaData[n]
142 = attributes().value(qualifiedName: QLatin1String("value")).toString();
143 else
144 metaData.insert(akey: n, avalue: attributes().
145 value(qualifiedName: QLatin1String("value")).toString());
146 } else {
147 skipUnknownToken();
148 }
149 } else if (isEndElement() && name() == QLatin1String("QtHelpProject")) {
150 if (namespaceName.isEmpty())
151 raiseError(message: QCoreApplication::translate(context: "QHelpProject",
152 key: "Missing namespace in QtHelpProject file: \"%1\"").arg(a: fileName));
153 else if (virtualFolder.isEmpty())
154 raiseError(message: QCoreApplication::translate(context: "QHelpProject",
155 key: "Missing virtual folder in QtHelpProject file: \"%1\"").arg(a: fileName));
156 break;
157 }
158 }
159}
160
161void QHelpProjectDataPrivate::readCustomFilter()
162{
163 QHelpDataCustomFilter filter;
164 filter.name = attributes().value(qualifiedName: QLatin1String("name")).toString();
165 while (!atEnd()) {
166 readNext();
167 if (isStartElement()) {
168 if (name() == QLatin1String("filterAttribute"))
169 filter.filterAttributes.append(t: readElementText());
170 else
171 skipUnknownToken();
172 } else if (isEndElement() && name() == QLatin1String("customFilter")) {
173 break;
174 }
175 }
176 customFilterList.append(t: filter);
177}
178
179void QHelpProjectDataPrivate::readFilterSection()
180{
181 filterSectionList.append(t: QHelpDataFilterSection());
182 while (!atEnd()) {
183 readNext();
184 if (isStartElement()) {
185 if (name() == QLatin1String("filterAttribute"))
186 filterSectionList.last().addFilterAttribute(filter: readElementText());
187 else if (name() == QLatin1String("toc"))
188 readTOC();
189 else if (name() == QLatin1String("keywords"))
190 readKeywords();
191 else if (name() == QLatin1String("files"))
192 readFiles();
193 else
194 skipUnknownToken();
195 } else if (isEndElement() && name() == QLatin1String("filterSection")) {
196 break;
197 }
198 }
199}
200
201void QHelpProjectDataPrivate::readTOC()
202{
203 QStack<QHelpDataContentItem*> contentStack;
204 QHelpDataContentItem *itm = nullptr;
205 while (!atEnd()) {
206 readNext();
207 if (isStartElement()) {
208 if (name() == QLatin1String("section")) {
209 const QString &title = attributes().value(qualifiedName: QLatin1String("title")).toString();
210 const QString &ref = attributes().value(qualifiedName: QLatin1String("ref")).toString();
211 if (contentStack.isEmpty()) {
212 itm = new QHelpDataContentItem(nullptr, title, ref);
213 filterSectionList.last().addContent(content: itm);
214 } else {
215 itm = new QHelpDataContentItem(contentStack.top(), title, ref);
216 }
217 contentStack.push(t: itm);
218 } else {
219 skipUnknownToken();
220 }
221 } else if (isEndElement()) {
222 if (name() == QLatin1String("section")) {
223 contentStack.pop();
224 continue;
225 } else if (name() == QLatin1String("toc") && contentStack.isEmpty()) {
226 return;
227 } else {
228 skipUnknownToken();
229 }
230 }
231 }
232}
233
234static inline QString msgMissingAttribute(const QString &fileName, qint64 lineNumber, const QString &name)
235{
236 QString result;
237 QTextStream str(&result);
238 str << QDir::toNativeSeparators(pathName: fileName) << ':' << lineNumber
239 << ": Missing attribute in <keyword";
240 if (!name.isEmpty())
241 str << " name=\"" << name << '"';
242 str << ">.";
243 return result;
244}
245
246void QHelpProjectDataPrivate::readKeywords()
247{
248 while (!atEnd()) {
249 readNext();
250 if (isStartElement()) {
251 if (name() == QLatin1String("keyword")) {
252 const QString &refAttribute = attributes().value(QStringLiteral("ref")).toString();
253 const QString &nameAttribute = attributes().value(QStringLiteral("name")).toString();
254 const QString &idAttribute = attributes().value(QStringLiteral("id")).toString();
255 if (refAttribute.isEmpty() || (nameAttribute.isEmpty() && idAttribute.isEmpty())) {
256 qWarning(msg: "%s", qPrintable(msgMissingAttribute(fileName, lineNumber(), nameAttribute)));
257 continue;
258 }
259 filterSectionList.last()
260 .addIndex(index: QHelpDataIndexItem(nameAttribute, idAttribute, refAttribute));
261 } else {
262 skipUnknownToken();
263 }
264 } else if (isEndElement()) {
265 if (name() == QLatin1String("keyword"))
266 continue;
267 else if (name() == QLatin1String("keywords"))
268 return;
269 else
270 skipUnknownToken();
271 }
272 }
273}
274
275void QHelpProjectDataPrivate::readFiles()
276{
277 while (!atEnd()) {
278 readNext();
279 if (isStartElement()) {
280 if (name() == QLatin1String("file"))
281 addMatchingFiles(pattern: readElementText());
282 else
283 skipUnknownToken();
284 } else if (isEndElement()) {
285 if (name() == QLatin1String("file"))
286 continue;
287 else if (name() == QLatin1String("files"))
288 return;
289 else
290 skipUnknownToken();
291 }
292 }
293}
294
295// Expand file pattern and add matches into list. If the pattern does not match
296// any files, insert the pattern itself so the QHelpGenerator will emit a
297// meaningful warning later.
298void QHelpProjectDataPrivate::addMatchingFiles(const QString &pattern)
299{
300 // The pattern matching is expensive, so we skip it if no
301 // wildcard symbols occur in the string.
302 if (!pattern.contains(c: QLatin1Char('?')) && !pattern.contains(c: QLatin1Char('*'))
303 && !pattern.contains(c: QLatin1Char('[')) && !pattern.contains(c: QLatin1Char(']'))) {
304 filterSectionList.last().addFile(file: pattern);
305 return;
306 }
307
308 const QFileInfo fileInfo(rootPath + QLatin1Char('/') + pattern);
309 const QDir &dir = fileInfo.dir();
310 const QString &path = dir.canonicalPath();
311
312 // QDir::entryList() is expensive, so we cache the results.
313 const auto &it = dirEntriesCache.constFind(akey: path);
314 const QStringList &entries = it != dirEntriesCache.cend() ?
315 it.value() : dir.entryList(filters: QDir::Files);
316 if (it == dirEntriesCache.cend())
317 dirEntriesCache.insert(akey: path, avalue: entries);
318
319 bool matchFound = false;
320#ifdef Q_OS_WIN
321 Qt::CaseSensitivity cs = Qt::CaseInsensitive;
322#else
323 Qt::CaseSensitivity cs = Qt::CaseSensitive;
324#endif
325 const QRegExp regExp(fileInfo.fileName(), cs, QRegExp::Wildcard);
326 for (const QString &file : entries) {
327 if (regExp.exactMatch(str: file)) {
328 matchFound = true;
329 filterSectionList.last().
330 addFile(file: QFileInfo(pattern).dir().path() + QLatin1Char('/') + file);
331 }
332 }
333 if (!matchFound)
334 filterSectionList.last().addFile(file: pattern);
335}
336
337bool QHelpProjectDataPrivate::hasValidSyntax(const QString &nameSpace,
338 const QString &vFolder) const
339{
340 const QLatin1Char slash('/');
341 if (nameSpace.contains(c: slash) || vFolder.contains(c: slash))
342 return false;
343 QUrl url;
344 const QLatin1String scheme("qthelp");
345 url.setScheme(scheme);
346 const QString &canonicalNamespace = nameSpace.toLower();
347 url.setHost(host: canonicalNamespace);
348 url.setPath(path: slash + vFolder);
349
350 const QString expectedUrl(scheme + QLatin1String("://")
351 + canonicalNamespace + slash + vFolder);
352 return url.isValid() && url.toString() == expectedUrl;
353}
354
355/*!
356 \internal
357 \class QHelpProjectData
358 \since 4.4
359 \brief The QHelpProjectData class stores all information found
360 in a Qt help project file.
361
362 The structure is filled with data by calling readData(). The
363 specified file has to have the Qt help project file format in
364 order to be read successfully. Possible reading errors can be
365 retrieved by calling errorMessage().
366*/
367
368/*!
369 Constructs a Qt help project data structure.
370*/
371QHelpProjectData::QHelpProjectData()
372{
373 d = new QHelpProjectDataPrivate;
374}
375
376/*!
377 Destroys the help project data.
378*/
379QHelpProjectData::~QHelpProjectData()
380{
381 delete d;
382}
383
384/*!
385 Reads the file \a fileName and stores the help data. The file has to
386 have the Qt help project file format. Returns true if the file
387 was successfully read, otherwise false.
388
389 \sa errorMessage()
390*/
391bool QHelpProjectData::readData(const QString &fileName)
392{
393 d->fileName = fileName;
394 d->rootPath = QFileInfo(fileName).absolutePath();
395 QFile file(fileName);
396 if (!file.open(flags: QIODevice::ReadOnly)) {
397 d->errorMsg = QCoreApplication::translate(context: "QHelpProject",
398 key: "The input file %1 could not be opened.").arg(a: fileName);
399 return false;
400 }
401
402 d->readData(contents: file.readAll());
403 return !d->hasError();
404}
405
406/*!
407 Returns an error message if the reading of the Qt help project
408 file failed. Otherwise, an empty QString is returned.
409
410 \sa readData()
411*/
412QString QHelpProjectData::errorMessage() const
413{
414 if (d->hasError())
415 return d->errorString();
416 return d->errorMsg;
417}
418
419/*!
420 \internal
421*/
422QString QHelpProjectData::namespaceName() const
423{
424 return d->namespaceName;
425}
426
427/*!
428 \internal
429*/
430QString QHelpProjectData::virtualFolder() const
431{
432 return d->virtualFolder;
433}
434
435/*!
436 \internal
437*/
438QList<QHelpDataCustomFilter> QHelpProjectData::customFilters() const
439{
440 return d->customFilterList;
441}
442
443/*!
444 \internal
445*/
446QList<QHelpDataFilterSection> QHelpProjectData::filterSections() const
447{
448 return d->filterSectionList;
449}
450
451/*!
452 \internal
453*/
454QMap<QString, QVariant> QHelpProjectData::metaData() const
455{
456 return d->metaData;
457}
458
459/*!
460 \internal
461*/
462QString QHelpProjectData::rootPath() const
463{
464 return d->rootPath;
465}
466
467QT_END_NAMESPACE
468

source code of qttools/src/assistant/qhelpgenerator/qhelpprojectdata.cpp