Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qhelpdbreader.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3// Qt-Security score:critical reason:data-parser
4
6#include "qhelp_global.h"
7
8#include <QtCore/qfile.h>
9#include <QtCore/qmap.h>
10#include <QtCore/qset.h>
11#include <QtCore/qvariant.h>
12#include <QtCore/qversionnumber.h>
13#include <QtSql/qsqldatabase.h>
14#include <QtSql/qsqlerror.h>
15#include <QtSql/qsqlquery.h>
16
18
19using namespace Qt::StringLiterals;
20
21QHelpDBReader::QHelpDBReader(const QString &dbName)
22 : m_dbName(dbName)
23 , m_uniqueId(QHelpGlobal::uniquifyConnectionName("QHelpDBReader"_L1, this))
24{}
25
26QHelpDBReader::QHelpDBReader(const QString &dbName, const QString &uniqueId, QObject *parent)
27 : QObject(parent)
28 , m_dbName(dbName)
29 , m_uniqueId(uniqueId)
30{}
31
33{
34 if (m_initDone)
35 QSqlDatabase::removeDatabase(m_uniqueId);
36}
37
39{
40 if (m_initDone)
41 return true;
42
43 if (!QFile::exists(m_dbName))
44 return false;
45
46 if (!initDB()) {
47 QSqlDatabase::removeDatabase(m_uniqueId);
48 return false;
49 }
50
51 m_initDone = true;
52 m_query.reset(new QSqlQuery(QSqlDatabase::database(m_uniqueId)));
53 return true;
54}
55
56bool QHelpDBReader::initDB()
57{
58 QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"_L1, m_uniqueId);
59 db.setConnectOptions("QSQLITE_OPEN_READONLY"_L1);
60 db.setDatabaseName(m_dbName);
61 if (!db.open()) {
62 /*: The placeholders are: %1 - The name of the database which cannot be opened
63 %2 - The unique id for the connection
64 %3 - The actual error string */
65 m_error = tr("Cannot open database \"%1\" \"%2\": %3").arg(m_dbName, m_uniqueId, db.lastError().text());
66 return false;
67 }
68 return true;
69}
70
72{
73 if (!m_namespace.isEmpty())
74 return m_namespace;
75 if (m_query) {
76 m_query->exec("SELECT Name FROM NamespaceTable"_L1);
77 if (m_query->next())
78 m_namespace = m_query->value(0).toString();
79 }
80 return m_namespace;
81}
82
84{
85 if (m_query) {
86 m_query->exec("SELECT Name FROM FolderTable WHERE Id=1"_L1);
87 if (m_query->next())
88 return m_query->value(0).toString();
89 }
90 return {};
91}
92
93QString QHelpDBReader::version() const
94{
95 const QString versionString = metaData("version"_L1).toString();
96 if (versionString.isEmpty())
97 return qtVersionHeuristic();
98 return versionString;
99}
100
102{
103 QString tail;
104 int major = -1;
105 int minor = -1;
106 int patch = -1;
107};
108
109static VersionData getVersionData(const QString &nameSpace)
110{
111 VersionData result;
112 if (!nameSpace.startsWith("org.qt-project."_L1))
113 return result;
114
115 // We take the namespace tail, starting from the last letter in namespace name.
116 // We drop any non digit characters.
117 const QChar dot(u'.');
118 QString &tail = result.tail;
119 for (int i = nameSpace.size(); i > 0; --i) {
120 const QChar c = nameSpace.at(i - 1);
121 if (c.isDigit() || c == dot)
122 tail.prepend(c);
123
124 if (c.isLetter())
125 break;
126 }
127
128 if (!tail.startsWith(dot) && tail.count(dot) == 1) {
129 // The org.qt-project.qtquickcontrols2.5120 case,
130 // tail = 2.5120 here. We need to cut "2." here.
131 const int dotIndex = tail.indexOf(dot);
132 if (dotIndex > 0)
133 tail = tail.mid(dotIndex);
134 }
135
136 // Drop beginning dots
137 while (tail.startsWith(dot))
138 tail = tail.mid(1);
139
140 // Drop ending dots
141 while (tail.endsWith(dot))
142 tail.chop(1);
143
144 if (tail.size() >= 3 && tail.size() <= 5 && tail.count(dot) == 0) {
145 // When we have 3 digits, we split it like: ABC -> A.B.C
146 // When we have 4 digits, we split it like: ABCD -> A.BC.D
147 // When we have 5 digits, we split it like: ABCDE -> A.BC.DE
148 QStringView tail{result.tail};
149 result.major = tail.left(1).toInt();
150 result.minor = tail.size() == 3 ? tail.mid(1, 1).toInt() : tail.mid(1, 2).toInt();
151 result.patch = tail.size() == 5 ? tail.right(2).toInt() : tail.right(1).toInt();
152 }
153 return result;
154}
155
156QString QHelpDBReader::qtVersionHeuristic() const
157{
158 VersionData version = getVersionData(namespaceName());
159 return version.major >= 0
160 ? QString::number(version.major) + u'.' + QString::number(version.minor) + u'.' + QString::number(version.patch)
161 : version.tail;
162}
163
164QVersionNumber QHelpDBReader::versionHeuristic(const QString &namespaceName)
165{
166 VersionData version = getVersionData(namespaceName);
167 return version.major >= 0 ? QVersionNumber{version.major, version.minor, version.patch}
168 : QVersionNumber::fromString(version.tail);
169}
170
171static bool isAttributeUsed(QSqlQuery *query, const QString &tableName, int attributeId)
172{
173 query->prepare(QString::fromLatin1("SELECT FilterAttributeId "
174 "FROM %1 "
175 "WHERE FilterAttributeId = ? "
176 "LIMIT 1").arg(tableName));
177 query->bindValue(0, attributeId);
178 query->exec();
179 return query->next(); // if we got a result it means it was used
180}
181
182static int filterDataCount(QSqlQuery *query, const QString &tableName)
183{
184 query->exec(QString::fromLatin1("SELECT COUNT(*) FROM"
185 "(SELECT DISTINCT * FROM %1)").arg(tableName));
186 query->next();
187 return query->value(0).toInt();
188}
189
191{
192 IndexTable table;
193 if (!m_query)
194 return table;
195
196 // Don't let the queries cache the already fetched rows, as all the data is copied
197 // into the table anyway.
198 m_query->setForwardOnly(true);
199
200 QMap<int, QString> attributeIds;
201 m_query->exec("SELECT DISTINCT Id, Name FROM FilterAttributeTable ORDER BY Id"_L1);
202 while (m_query->next())
203 attributeIds.insert(m_query->value(0).toInt(), m_query->value(1).toString());
204
205 // Maybe some are unused and specified erroneously in the named filter only,
206 // like it was in case of qtlocation.qch <= qt 5.9
207 QList<int> usedAttributeIds;
208 for (auto it = attributeIds.cbegin(), end = attributeIds.cend(); it != end; ++it) {
209 const int attributeId = it.key();
210 if (isAttributeUsed(m_query.get(), "IndexFilterTable"_L1, attributeId)
211 || isAttributeUsed(m_query.get(), "ContentsFilterTable"_L1, attributeId)
212 || isAttributeUsed(m_query.get(), "FileFilterTable"_L1, attributeId)) {
213 usedAttributeIds.append(attributeId);
214 }
215 }
216
217 bool legacy = false;
218 m_query->exec("SELECT * FROM pragma_table_info('IndexTable') WHERE name='ContextName'"_L1);
219 if (m_query->next())
220 legacy = true;
221
222 const QString identifierColumnName = legacy ? "ContextName"_L1 : "Identifier"_L1;
223 const int usedAttributeCount = usedAttributeIds.size();
224
225 QMap<int, IndexItem> idToIndexItem;
226 m_query->exec(QString::fromLatin1("SELECT Name, %1, FileId, Anchor, Id "
227 "FROM IndexTable "
228 "ORDER BY Id").arg(identifierColumnName));
229 while (m_query->next()) {
230 IndexItem indexItem;
231 indexItem.name = m_query->value(0).toString();
232 indexItem.identifier = m_query->value(1).toString();
233 indexItem.fileId = m_query->value(2).toInt();
234 indexItem.anchor = m_query->value(3).toString();
235 const int indexId = m_query->value(4).toInt();
236
237 idToIndexItem.insert(indexId, indexItem);
238 }
239
240 QMap<int, FileItem> idToFileItem;
241 QMap<int, int> originalFileIdToNewFileId;
242
243 int filesCount = 0;
244 const bool filesRead = m_query->exec(
245 "SELECT "
246 "FileNameTable.FileId, "
247 "FileNameTable.Name, "
248 "FileNameTable.Title "
249 "FROM FileNameTable, FolderTable "
250 "WHERE FileNameTable.FolderId = FolderTable.Id "
251 "ORDER BY FileId"_L1);
252 if (!filesRead) {
253 m_query->finish();
254 m_query->setForwardOnly(false);
255 return table;
256 }
257 while (m_query->next()) {
258 const int fileId = m_query->value(0).toInt();
259 FileItem fileItem;
260 fileItem.name = m_query->value(1).toString();
261 fileItem.title = m_query->value(2).toString();
262
263 idToFileItem.insert(fileId, fileItem);
264 originalFileIdToNewFileId.insert(fileId, filesCount);
265 ++filesCount;
266 }
267
268 QMap<int, ContentsItem> idToContentsItem;
269
270 m_query->exec("SELECT Data, Id FROM ContentsTable ORDER BY Id"_L1);
271 while (m_query->next()) {
272 ContentsItem contentsItem;
273 contentsItem.data = m_query->value(0).toByteArray();
274 const int contentsId = m_query->value(1).toInt();
275
276 idToContentsItem.insert(contentsId, contentsItem);
277 }
278
279 bool optimized = true;
280
281 if (usedAttributeCount) {
282 // May optimize only when all usedAttributes are attached to every
283 // index and file. It means the number of rows in the
284 // IndexTable multiplied by number of used attributes
285 // must equal the number of rows inside IndexFilterTable
286 // (yes, we have a combinatorial explosion of data in IndexFilterTable,
287 // which we want to optimize). The same with FileNameTable and
288 // FileFilterTable.
289
290 const bool mayOptimizeIndexTable = filterDataCount(m_query.get(), "IndexFilterTable"_L1)
291 == idToIndexItem.size() * usedAttributeCount;
292 const bool mayOptimizeFileTable = filterDataCount(m_query.get(), "FileFilterTable"_L1)
293 == idToFileItem.size() * usedAttributeCount;
294 const bool mayOptimizeContentsTable =
295 filterDataCount(m_query.get(), "ContentsFilterTable"_L1)
296 == idToContentsItem.size() * usedAttributeCount;
297 optimized = mayOptimizeIndexTable && mayOptimizeFileTable && mayOptimizeContentsTable;
298
299 if (!optimized) {
300 m_query->exec(
301 "SELECT "
302 "IndexFilterTable.IndexId, "
303 "FilterAttributeTable.Name "
304 "FROM "
305 "IndexFilterTable, "
306 "FilterAttributeTable "
307 "WHERE "
308 "IndexFilterTable.FilterAttributeId = FilterAttributeTable.Id"_L1);
309 while (m_query->next()) {
310 const int indexId = m_query->value(0).toInt();
311 auto it = idToIndexItem.find(indexId);
312 if (it != idToIndexItem.end())
313 it.value().filterAttributes.append(m_query->value(1).toString());
314 }
315
316 m_query->exec(
317 "SELECT "
318 "FileFilterTable.FileId, "
319 "FilterAttributeTable.Name "
320 "FROM "
321 "FileFilterTable, "
322 "FilterAttributeTable "
323 "WHERE "
324 "FileFilterTable.FilterAttributeId = FilterAttributeTable.Id"_L1);
325 while (m_query->next()) {
326 const int fileId = m_query->value(0).toInt();
327 auto it = idToFileItem.find(fileId);
328 if (it != idToFileItem.end())
329 it.value().filterAttributes.append(m_query->value(1).toString());
330 }
331
332 m_query->exec(
333 "SELECT "
334 "ContentsFilterTable.ContentsId, "
335 "FilterAttributeTable.Name "
336 "FROM "
337 "ContentsFilterTable, "
338 "FilterAttributeTable "
339 "WHERE "
340 "ContentsFilterTable.FilterAttributeId = FilterAttributeTable.Id"_L1);
341 while (m_query->next()) {
342 const int contentsId = m_query->value(0).toInt();
343 auto it = idToContentsItem.find(contentsId);
344 if (it != idToContentsItem.end())
345 it.value().filterAttributes.append(m_query->value(1).toString());
346 }
347 }
348 }
349
350 // The help generator writes a "file not found" entry, which has no folder and no name, and
351 // makes the keywords referencing files missing in the documentation point at it.
352 // Such keywords have no target, so they are left out.
353 QSet<int> fileNotFoundIds;
354 m_query->exec("SELECT FileId FROM FileNameTable WHERE FolderId = 0 AND Name = ''"_L1);
355 while (m_query->next())
356 fileNotFoundIds.insert(m_query->value(0).toInt());
357
358 // reindex fileId references
359 for (auto it = idToIndexItem.cbegin(), end = idToIndexItem.cend(); it != end; ++it) {
360 IndexItem item = it.value();
361 if (fileNotFoundIds.contains(item.fileId))
362 continue;
363 // An unknown file id is kept invalid, it's rejected when registering the index.
364 item.fileId = originalFileIdToNewFileId.value(item.fileId, -1);
365 table.indexItems.append(item);
366 }
367
368 table.valid = true;
369 table.fileItems = idToFileItem.values();
370 table.contentsItems = idToContentsItem.values();
371
372 if (optimized) {
373 for (int attributeId : std::as_const(usedAttributeIds))
374 table.usedFilterAttributes.append(attributeIds.value(attributeId));
375 }
376 m_query->finish();
377 m_query->setForwardOnly(false);
378 return table;
379}
380
382{
383 QList<QStringList> result;
384 if (m_query) {
385 m_query->exec(
386 "SELECT "
387 "FileAttributeSetTable.Id, "
388 "FilterAttributeTable.Name "
389 "FROM "
390 "FileAttributeSetTable, "
391 "FilterAttributeTable "
392 "WHERE FileAttributeSetTable.FilterAttributeId = FilterAttributeTable.Id "
393 "ORDER BY FileAttributeSetTable.Id"_L1);
394 int oldId = -1;
395 while (m_query->next()) {
396 const int id = m_query->value(0).toInt();
397 if (id != oldId) {
398 result.append(QStringList());
399 oldId = id;
400 }
401 result.last().append(m_query->value(1).toString());
402 }
403 }
404 return result;
405}
406
407QByteArray QHelpDBReader::fileData(const QString &virtualFolder,
408 const QString &filePath) const
409{
410 QByteArray ba;
411 if (virtualFolder.isEmpty() || filePath.isEmpty() || !m_query)
412 return ba;
413
414 namespaceName();
415 m_query->prepare(
416 "SELECT "
417 "FileDataTable.Data "
418 "FROM "
419 "FileDataTable, "
420 "FileNameTable, "
421 "FolderTable, "
422 "NamespaceTable "
423 "WHERE FileDataTable.Id = FileNameTable.FileId "
424 "AND (FileNameTable.Name = ? OR FileNameTable.Name = ?) "
425 "AND FileNameTable.FolderId = FolderTable.Id "
426 "AND FolderTable.Name = ? "
427 "AND FolderTable.NamespaceId = NamespaceTable.Id "
428 "AND NamespaceTable.Name = ?"_L1);
429 m_query->bindValue(0, filePath);
430 m_query->bindValue(1, QString("./"_L1 + filePath));
431 m_query->bindValue(2, virtualFolder);
432 m_query->bindValue(3, m_namespace);
433 m_query->exec();
434 if (m_query->next() && m_query->isValid())
435 ba = qUncompress(m_query->value(0).toByteArray());
436 return ba;
437}
438
440{
441 QStringList lst;
442 if (m_query) {
443 m_query->exec("SELECT Name FROM FilterNameTable"_L1);
444 while (m_query->next())
445 lst.append(m_query->value(0).toString());
446 }
447 return lst;
448}
449
450QStringList QHelpDBReader::filterAttributes(const QString &filterName) const
451{
452 QStringList lst;
453 if (m_query) {
454 if (filterName.isEmpty()) {
455 m_query->prepare("SELECT Name FROM FilterAttributeTable"_L1);
456 } else {
457 m_query->prepare(
458 "SELECT "
459 "FilterAttributeTable.Name "
460 "FROM "
461 "FilterAttributeTable, "
462 "FilterTable, "
463 "FilterNameTable "
464 "WHERE FilterNameTable.Name = ? "
465 "AND FilterNameTable.Id = FilterTable.NameId "
466 "AND FilterTable.FilterAttributeId = FilterAttributeTable.Id"_L1);
467 m_query->bindValue(0, filterName);
468 }
469 m_query->exec();
470 while (m_query->next())
471 lst.append(m_query->value(0).toString());
472 }
473 return lst;
474}
475
476void QHelpDBReader::filesData(const QStringList &filterAttributes,
477 const QStringList &extensionFilters,
478 const std::function<bool(const QString &fileName,
479 const QByteArray &data)> &handler) const
480{
481 if (!m_query)
482 return;
483
484 QString query;
485 QString extension;
486 if (!extensionFilters.isEmpty()) {
487 QStringList conditions;
488 for (const QString &extensionFilter : extensionFilters)
489 conditions.append("FileNameTable.Name LIKE \'%.%1\'"_L1.arg(extensionFilter));
490 extension = "AND (%1)"_L1.arg(conditions.join(" OR "_L1));
491 }
492
493 if (filterAttributes.isEmpty()) {
494 query =
495 "SELECT "
496 "FileNameTable.Name, "
497 "FileDataTable.Data "
498 "FROM "
499 "FolderTable, "
500 "FileNameTable, "
501 "FileDataTable "
502 "WHERE FileDataTable.Id = FileNameTable.FileId "
503 "AND FileNameTable.FolderId = FolderTable.Id %1"_L1.arg(extension);
504 } else {
505 for (int i = 0; i < filterAttributes.size(); ++i) {
506 if (i > 0)
507 query.append(" INTERSECT "_L1);
508 query.append(
509 "SELECT "
510 "FileNameTable.Name, "
511 "FileDataTable.Data "
512 "FROM "
513 "FolderTable, "
514 "FileNameTable, "
515 "FileDataTable, "
516 "FileFilterTable, "
517 "FilterAttributeTable "
518 "WHERE FileDataTable.Id = FileNameTable.FileId "
519 "AND FileNameTable.FolderId = FolderTable.Id "
520 "AND FileNameTable.FileId = FileFilterTable.FileId "
521 "AND FileFilterTable.FilterAttributeId = FilterAttributeTable.Id "
522 "AND FilterAttributeTable.Name = \'%1\' %2"_L1
523 .arg(quote(filterAttributes.at(i)), extension));
524 }
525 }
526 // Don't let the query cache the already fetched rows, so that only one file is held
527 // in memory at a time.
528 m_query->setForwardOnly(true);
529 m_query->exec(query);
530 while (m_query->next()) {
531 if (!handler(m_query->value(0).toString(),
532 qUncompress(m_query->value(1).toByteArray()))) {
533 break;
534 }
535 }
536 m_query->finish();
537 m_query->setForwardOnly(false);
538}
539
540QVariant QHelpDBReader::metaData(const QString &name) const
541{
542 if (!m_query)
543 return {};
544
545 m_query->prepare("SELECT COUNT(Value), Value FROM MetaDataTable WHERE Name=?"_L1);
546 m_query->bindValue(0, name);
547 if (m_query->exec() && m_query->next() && m_query->value(0).toInt() == 1)
548 return m_query->value(1);
549 return {};
550}
551
552QString QHelpDBReader::quote(const QString &string) const
553{
554 QString s = string;
555 s.replace(u'\'', "\'\'"_L1);
556 return s;
557}
558
559QT_END_NAMESPACE
QString namespaceName() const
IndexTable indexTable() const
QString virtualFolder() const
QString version() const
QList< QStringList > filterAttributeSets() const
QStringList customFilters() const
\inmodule QtCore
Combined button and popup list for selecting options.
static bool isAttributeUsed(QSqlQuery *query, const QString &tableName, int attributeId)
static VersionData getVersionData(const QString &nameSpace)
static int filterDataCount(QSqlQuery *query, const QString &tableName)