Revision control

Copy as Markdown

// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:critical reason:data-parser
#include "qhelpdbreader_p.h"
#include "qhelp_global.h"
#include <QtCore/qfile.h>
#include <QtCore/qmap.h>
#include <QtCore/qvariant.h>
#include <QtCore/qversionnumber.h>
#include <QtSql/qsqldatabase.h>
#include <QtSql/qsqlerror.h>
#include <QtSql/qsqlquery.h>
QT_BEGIN_NAMESPACE
using namespace Qt::StringLiterals;
QHelpDBReader::QHelpDBReader(const QString &dbName)
: m_dbName(dbName)
, m_uniqueId(QHelpGlobal::uniquifyConnectionName("QHelpDBReader"_L1, this))
{}
QHelpDBReader::QHelpDBReader(const QString &dbName, const QString &uniqueId, QObject *parent)
: QObject(parent)
, m_dbName(dbName)
, m_uniqueId(uniqueId)
{}
QHelpDBReader::~QHelpDBReader()
{
if (m_initDone)
QSqlDatabase::removeDatabase(m_uniqueId);
}
bool QHelpDBReader::init()
{
if (m_initDone)
return true;
if (!QFile::exists(m_dbName))
return false;
if (!initDB()) {
QSqlDatabase::removeDatabase(m_uniqueId);
return false;
}
m_initDone = true;
m_query.reset(new QSqlQuery(QSqlDatabase::database(m_uniqueId)));
return true;
}
bool QHelpDBReader::initDB()
{
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"_L1, m_uniqueId);
db.setConnectOptions("QSQLITE_OPEN_READONLY"_L1);
db.setDatabaseName(m_dbName);
if (!db.open()) {
/*: The placeholders are: %1 - The name of the database which cannot be opened
%2 - The unique id for the connection
%3 - The actual error string */
m_error = tr("Cannot open database \"%1\" \"%2\": %3").arg(m_dbName, m_uniqueId, db.lastError().text());
return false;
}
return true;
}
QString QHelpDBReader::namespaceName() const
{
if (!m_namespace.isEmpty())
return m_namespace;
if (m_query) {
m_query->exec("SELECT Name FROM NamespaceTable"_L1);
if (m_query->next())
m_namespace = m_query->value(0).toString();
}
return m_namespace;
}
QString QHelpDBReader::virtualFolder() const
{
if (m_query) {
m_query->exec("SELECT Name FROM FolderTable WHERE Id=1"_L1);
if (m_query->next())
return m_query->value(0).toString();
}
return {};
}
QString QHelpDBReader::version() const
{
const QString versionString = metaData("version"_L1).toString();
if (versionString.isEmpty())
return qtVersionHeuristic();
return versionString;
}
struct VersionData
{
QString tail;
int major = -1;
int minor = -1;
int patch = -1;
};
static VersionData getVersionData(const QString &nameSpace)
{
VersionData result;
if (!nameSpace.startsWith("org.qt-project."_L1))
return result;
// We take the namespace tail, starting from the last letter in namespace name.
// We drop any non digit characters.
const QChar dot(u'.');
QString &tail = result.tail;
for (int i = nameSpace.size(); i > 0; --i) {
const QChar c = nameSpace.at(i - 1);
if (c.isDigit() || c == dot)
tail.prepend(c);
if (c.isLetter())
break;
}
if (!tail.startsWith(dot) && tail.count(dot) == 1) {
// The org.qt-project.qtquickcontrols2.5120 case,
// tail = 2.5120 here. We need to cut "2." here.
const int dotIndex = tail.indexOf(dot);
if (dotIndex > 0)
tail = tail.mid(dotIndex);
}
// Drop beginning dots
while (tail.startsWith(dot))
tail = tail.mid(1);
// Drop ending dots
while (tail.endsWith(dot))
tail.chop(1);
if (tail.size() >= 3 && tail.size() <= 5 && tail.count(dot) == 0) {
// When we have 3 digits, we split it like: ABC -> A.B.C
// When we have 4 digits, we split it like: ABCD -> A.BC.D
// When we have 5 digits, we split it like: ABCDE -> A.BC.DE
QStringView tail{result.tail};
result.major = tail.left(1).toInt();
result.minor = tail.size() == 3 ? tail.mid(1, 1).toInt() : tail.mid(1, 2).toInt();
result.patch = tail.size() == 5 ? tail.right(2).toInt() : tail.right(1).toInt();
}
return result;
}
QString QHelpDBReader::qtVersionHeuristic() const
{
VersionData version = getVersionData(namespaceName());
return version.major >= 0
? QString::number(version.major) + u'.' + QString::number(version.minor) + u'.' + QString::number(version.patch)
: version.tail;
}
QVersionNumber QHelpDBReader::versionHeuristic(const QString &namespaceName)
{
VersionData version = getVersionData(namespaceName);
return version.major >= 0 ? QVersionNumber{version.major, version.minor, version.patch}
: QVersionNumber::fromString(version.tail);
}
static bool isAttributeUsed(QSqlQuery *query, const QString &tableName, int attributeId)
{
query->prepare(QString::fromLatin1("SELECT FilterAttributeId "
"FROM %1 "
"WHERE FilterAttributeId = ? "
"LIMIT 1").arg(tableName));
query->bindValue(0, attributeId);
query->exec();
return query->next(); // if we got a result it means it was used
}
static int filterDataCount(QSqlQuery *query, const QString &tableName)
{
query->exec(QString::fromLatin1("SELECT COUNT(*) FROM"
"(SELECT DISTINCT * FROM %1)").arg(tableName));
query->next();
return query->value(0).toInt();
}
QHelpDBReader::IndexTable QHelpDBReader::indexTable() const
{
IndexTable table;
if (!m_query)
return table;
// Don't let the queries cache the already fetched rows, as all the data is copied
// into the table anyway.
m_query->setForwardOnly(true);
QMap<int, QString> attributeIds;
m_query->exec("SELECT DISTINCT Id, Name FROM FilterAttributeTable ORDER BY Id"_L1);
while (m_query->next())
attributeIds.insert(m_query->value(0).toInt(), m_query->value(1).toString());
// Maybe some are unused and specified erroneously in the named filter only,
// like it was in case of qtlocation.qch <= qt 5.9
QList<int> usedAttributeIds;
for (auto it = attributeIds.cbegin(), end = attributeIds.cend(); it != end; ++it) {
const int attributeId = it.key();
if (isAttributeUsed(m_query.get(), "IndexFilterTable"_L1, attributeId)
|| isAttributeUsed(m_query.get(), "ContentsFilterTable"_L1, attributeId)
|| isAttributeUsed(m_query.get(), "FileFilterTable"_L1, attributeId)) {
usedAttributeIds.append(attributeId);
}
}
bool legacy = false;
m_query->exec("SELECT * FROM pragma_table_info('IndexTable') WHERE name='ContextName'"_L1);
if (m_query->next())
legacy = true;
const QString identifierColumnName = legacy ? "ContextName"_L1 : "Identifier"_L1;
const int usedAttributeCount = usedAttributeIds.size();
QMap<int, IndexItem> idToIndexItem;
m_query->exec(QString::fromLatin1("SELECT Name, %1, FileId, Anchor, Id "
"FROM IndexTable "
"ORDER BY Id").arg(identifierColumnName));
while (m_query->next()) {
IndexItem indexItem;
indexItem.name = m_query->value(0).toString();
indexItem.identifier = m_query->value(1).toString();
indexItem.fileId = m_query->value(2).toInt();
indexItem.anchor = m_query->value(3).toString();
const int indexId = m_query->value(4).toInt();
idToIndexItem.insert(indexId, indexItem);
}
QMap<int, FileItem> idToFileItem;
QMap<int, int> originalFileIdToNewFileId;
int filesCount = 0;
m_query->exec(
"SELECT "
"FileNameTable.FileId, "
"FileNameTable.Name, "
"FileNameTable.Title "
"FROM FileNameTable, FolderTable "
"WHERE FileNameTable.FolderId = FolderTable.Id "
"ORDER BY FileId"_L1);
while (m_query->next()) {
const int fileId = m_query->value(0).toInt();
FileItem fileItem;
fileItem.name = m_query->value(1).toString();
fileItem.title = m_query->value(2).toString();
idToFileItem.insert(fileId, fileItem);
originalFileIdToNewFileId.insert(fileId, filesCount);
++filesCount;
}
QMap<int, ContentsItem> idToContentsItem;
m_query->exec("SELECT Data, Id FROM ContentsTable ORDER BY Id"_L1);
while (m_query->next()) {
ContentsItem contentsItem;
contentsItem.data = m_query->value(0).toByteArray();
const int contentsId = m_query->value(1).toInt();
idToContentsItem.insert(contentsId, contentsItem);
}
bool optimized = true;
if (usedAttributeCount) {
// May optimize only when all usedAttributes are attached to every
// index and file. It means the number of rows in the
// IndexTable multiplied by number of used attributes
// must equal the number of rows inside IndexFilterTable
// (yes, we have a combinatorial explosion of data in IndexFilterTable,
// which we want to optimize). The same with FileNameTable and
// FileFilterTable.
const bool mayOptimizeIndexTable = filterDataCount(m_query.get(), "IndexFilterTable"_L1)
== idToIndexItem.size() * usedAttributeCount;
const bool mayOptimizeFileTable = filterDataCount(m_query.get(), "FileFilterTable"_L1)
== idToFileItem.size() * usedAttributeCount;
const bool mayOptimizeContentsTable =
filterDataCount(m_query.get(), "ContentsFilterTable"_L1)
== idToContentsItem.size() * usedAttributeCount;
optimized = mayOptimizeIndexTable && mayOptimizeFileTable && mayOptimizeContentsTable;
if (!optimized) {
m_query->exec(
"SELECT "
"IndexFilterTable.IndexId, "
"FilterAttributeTable.Name "
"FROM "
"IndexFilterTable, "
"FilterAttributeTable "
"WHERE "
"IndexFilterTable.FilterAttributeId = FilterAttributeTable.Id"_L1);
while (m_query->next()) {
const int indexId = m_query->value(0).toInt();
auto it = idToIndexItem.find(indexId);
if (it != idToIndexItem.end())
it.value().filterAttributes.append(m_query->value(1).toString());
}
m_query->exec(
"SELECT "
"FileFilterTable.FileId, "
"FilterAttributeTable.Name "
"FROM "
"FileFilterTable, "
"FilterAttributeTable "
"WHERE "
"FileFilterTable.FilterAttributeId = FilterAttributeTable.Id"_L1);
while (m_query->next()) {
const int fileId = m_query->value(0).toInt();
auto it = idToFileItem.find(fileId);
if (it != idToFileItem.end())
it.value().filterAttributes.append(m_query->value(1).toString());
}
m_query->exec(
"SELECT "
"ContentsFilterTable.ContentsId, "
"FilterAttributeTable.Name "
"FROM "
"ContentsFilterTable, "
"FilterAttributeTable "
"WHERE "
"ContentsFilterTable.FilterAttributeId = FilterAttributeTable.Id"_L1);
while (m_query->next()) {
const int contentsId = m_query->value(0).toInt();
auto it = idToContentsItem.find(contentsId);
if (it != idToContentsItem.end())
it.value().filterAttributes.append(m_query->value(1).toString());
}
}
}
// reindex fileId references
for (auto it = idToIndexItem.cbegin(), end = idToIndexItem.cend(); it != end; ++it) {
IndexItem item = it.value();
// An unknown file id is kept invalid, it's rejected when registering the index.
item.fileId = originalFileIdToNewFileId.value(item.fileId, -1);
table.indexItems.append(item);
}
table.fileItems = idToFileItem.values();
table.contentsItems = idToContentsItem.values();
if (optimized) {
for (int attributeId : std::as_const(usedAttributeIds))
table.usedFilterAttributes.append(attributeIds.value(attributeId));
}
m_query->finish();
m_query->setForwardOnly(false);
return table;
}
QList<QStringList> QHelpDBReader::filterAttributeSets() const
{
QList<QStringList> result;
if (m_query) {
m_query->exec(
"SELECT "
"FileAttributeSetTable.Id, "
"FilterAttributeTable.Name "
"FROM "
"FileAttributeSetTable, "
"FilterAttributeTable "
"WHERE FileAttributeSetTable.FilterAttributeId = FilterAttributeTable.Id "
"ORDER BY FileAttributeSetTable.Id"_L1);
int oldId = -1;
while (m_query->next()) {
const int id = m_query->value(0).toInt();
if (id != oldId) {
result.append(QStringList());
oldId = id;
}
result.last().append(m_query->value(1).toString());
}
}
return result;
}
QByteArray QHelpDBReader::fileData(const QString &virtualFolder,
const QString &filePath) const
{
QByteArray ba;
if (virtualFolder.isEmpty() || filePath.isEmpty() || !m_query)
return ba;
namespaceName();
m_query->prepare(
"SELECT "
"FileDataTable.Data "
"FROM "
"FileDataTable, "
"FileNameTable, "
"FolderTable, "
"NamespaceTable "
"WHERE FileDataTable.Id = FileNameTable.FileId "
"AND (FileNameTable.Name = ? OR FileNameTable.Name = ?) "
"AND FileNameTable.FolderId = FolderTable.Id "
"AND FolderTable.Name = ? "
"AND FolderTable.NamespaceId = NamespaceTable.Id "
"AND NamespaceTable.Name = ?"_L1);
m_query->bindValue(0, filePath);
m_query->bindValue(1, QString("./"_L1 + filePath));
m_query->bindValue(2, virtualFolder);
m_query->bindValue(3, m_namespace);
m_query->exec();
if (m_query->next() && m_query->isValid())
ba = qUncompress(m_query->value(0).toByteArray());
return ba;
}
QStringList QHelpDBReader::customFilters() const
{
QStringList lst;
if (m_query) {
m_query->exec("SELECT Name FROM FilterNameTable"_L1);
while (m_query->next())
lst.append(m_query->value(0).toString());
}
return lst;
}
QStringList QHelpDBReader::filterAttributes(const QString &filterName) const
{
QStringList lst;
if (m_query) {
if (filterName.isEmpty()) {
m_query->prepare("SELECT Name FROM FilterAttributeTable"_L1);
} else {
m_query->prepare(
"SELECT "
"FilterAttributeTable.Name "
"FROM "
"FilterAttributeTable, "
"FilterTable, "
"FilterNameTable "
"WHERE FilterNameTable.Name = ? "
"AND FilterNameTable.Id = FilterTable.NameId "
"AND FilterTable.FilterAttributeId = FilterAttributeTable.Id"_L1);
m_query->bindValue(0, filterName);
}
m_query->exec();
while (m_query->next())
lst.append(m_query->value(0).toString());
}
return lst;
}
void QHelpDBReader::filesData(const QStringList &filterAttributes,
const QStringList &extensionFilters,
const std::function<bool(const QString &fileName,
const QByteArray &data)> &handler) const
{
if (!m_query)
return;
QString query;
QString extension;
if (!extensionFilters.isEmpty()) {
QStringList conditions;
for (const QString &extensionFilter : extensionFilters)
conditions.append("FileNameTable.Name LIKE \'%.%1\'"_L1.arg(extensionFilter));
extension = "AND (%1)"_L1.arg(conditions.join(" OR "_L1));
}
if (filterAttributes.isEmpty()) {
query =
"SELECT "
"FileNameTable.Name, "
"FileDataTable.Data "
"FROM "
"FolderTable, "
"FileNameTable, "
"FileDataTable "
"WHERE FileDataTable.Id = FileNameTable.FileId "
"AND FileNameTable.FolderId = FolderTable.Id %1"_L1.arg(extension);
} else {
for (int i = 0; i < filterAttributes.size(); ++i) {
if (i > 0)
query.append(" INTERSECT "_L1);
query.append(
"SELECT "
"FileNameTable.Name, "
"FileDataTable.Data "
"FROM "
"FolderTable, "
"FileNameTable, "
"FileDataTable, "
"FileFilterTable, "
"FilterAttributeTable "
"WHERE FileDataTable.Id = FileNameTable.FileId "
"AND FileNameTable.FolderId = FolderTable.Id "
"AND FileNameTable.FileId = FileFilterTable.FileId "
"AND FileFilterTable.FilterAttributeId = FilterAttributeTable.Id "
"AND FilterAttributeTable.Name = \'%1\' %2"_L1
.arg(quote(filterAttributes.at(i)), extension));
}
}
// Don't let the query cache the already fetched rows, so that only one file is held
// in memory at a time.
m_query->setForwardOnly(true);
m_query->exec(query);
while (m_query->next()) {
if (!handler(m_query->value(0).toString(),
qUncompress(m_query->value(1).toByteArray()))) {
break;
}
}
m_query->finish();
m_query->setForwardOnly(false);
}
QVariant QHelpDBReader::metaData(const QString &name) const
{
if (!m_query)
return {};
m_query->prepare("SELECT COUNT(Value), Value FROM MetaDataTable WHERE Name=?"_L1);
m_query->bindValue(0, name);
if (m_query->exec() && m_query->next() && m_query->value(0).toInt() == 1)
return m_query->value(1);
return {};
}
QString QHelpDBReader::quote(const QString &string) const
{
QString s = string;
s.replace(u'\'', "\'\'"_L1);
return s;
}
QT_END_NAMESPACE