From ae4086f7a0396b1eeb6613f45e03e3c8a179162e Mon Sep 17 00:00:00 2001 From: Oleg Shparber Date: Thu, 13 Aug 2026 22:24:25 +0300 Subject: [PATCH] fix(util,registry): tolerate broken Info.plist (#1951) Fixes #1949. --- src/libs/registry/docset.cpp | 1 - src/libs/util/plist.cpp | 3 +- src/libs/util/tests/CMakeLists.txt | 6 ++ src/libs/util/tests/plist_test.cpp | 116 +++++++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 src/libs/util/tests/plist_test.cpp diff --git a/src/libs/registry/docset.cpp b/src/libs/registry/docset.cpp index 5082fc53..9e082c82 100644 --- a/src/libs/registry/docset.cpp +++ b/src/libs/registry/docset.cpp @@ -104,7 +104,6 @@ Docset::Docset(QString path) if (plist.hasError()) { qCWarning(log, "Failed to parse 'Info.plist' for docset at '%s'.", qPrintable(m_path)); - return; } if (m_name.isEmpty()) { diff --git a/src/libs/util/plist.cpp b/src/libs/util/plist.cpp index 8333aafa..15867305 100644 --- a/src/libs/util/plist.cpp +++ b/src/libs/util/plist.cpp @@ -22,7 +22,8 @@ bool Plist::read(const QString &fileName) return false; } - QXmlStreamReader xml(&file); + // Some docsets ship an Info.plist with whitespace before the XML declaration. + QXmlStreamReader xml(file.readAll().trimmed()); while (!xml.atEnd()) { const QXmlStreamReader::TokenType token = xml.readNext(); diff --git a/src/libs/util/tests/CMakeLists.txt b/src/libs/util/tests/CMakeLists.txt index 24c33390..73458400 100644 --- a/src/libs/util/tests/CMakeLists.txt +++ b/src/libs/util/tests/CMakeLists.txt @@ -6,6 +6,12 @@ target_link_libraries(fuzzy_test PRIVATE Util Qt6::Test) zeal_add_test(fuzzy_test) +# Property list tests +add_executable(plist_test plist_test.cpp) +target_link_libraries(plist_test PRIVATE Util Qt6::Test) + +zeal_add_test(plist_test) + # SQLite Statement tests add_executable(statement_test statement_test.cpp) target_link_libraries(statement_test PRIVATE Util Qt6::Test) diff --git a/src/libs/util/tests/plist_test.cpp b/src/libs/util/tests/plist_test.cpp new file mode 100644 index 00000000..940628da --- /dev/null +++ b/src/libs/util/tests/plist_test.cpp @@ -0,0 +1,116 @@ +// Copyright (C) Oleg Shparber, et al. +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "../plist.h" + +#include + +using namespace Zeal::Util; + +class PlistTest : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + + void testReadsKeys(); + void testLeadingNewline(); + void testLeadingIndentation(); + void testByteOrderMark(); + void testMalformedContent(); + void testMissingFile(); + +private: + QString writePlist(const QByteArray &data); + + QTemporaryDir m_dir; + int m_counter = 0; +}; + +namespace { +const QByteArray Body = QByteArrayLiteral("\n" + "\n" + "\n" + " \n" + " CFBundleName\n" + " Android KTX\n" + " isDashDocset\n" + " \n" + " isJavaScriptEnabled\n" + " \n" + " \n" + "\n"); +} // namespace + +QString PlistTest::writePlist(const QByteArray &data) +{ + const QString path = m_dir.filePath(QStringLiteral("Info%1.plist").arg(++m_counter)); + QFile file(path); + if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size()) { + return {}; + } + + return path; +} + +void PlistTest::initTestCase() +{ + QVERIFY(m_dir.isValid()); +} + +void PlistTest::testReadsKeys() +{ + Plist plist; + QVERIFY(plist.read(writePlist(Body))); + QVERIFY(!plist.hasError()); + QCOMPARE(plist.value(QStringLiteral("CFBundleName")), QVariant(QStringLiteral("Android KTX"))); + QCOMPARE(plist.value(QStringLiteral("isDashDocset")), QVariant(true)); + QCOMPARE(plist.value(QStringLiteral("isJavaScriptEnabled")), QVariant(false)); +} + +// See: https://github.com/zealdocs/zeal/issues/1949 +void PlistTest::testLeadingNewline() +{ + Plist plist; + QVERIFY(plist.read(writePlist(QByteArrayLiteral("\n") + Body))); + QVERIFY(!plist.hasError()); + QCOMPARE(plist[QStringLiteral("CFBundleName")].toString(), QStringLiteral("Android KTX")); +} + +void PlistTest::testLeadingIndentation() +{ + Plist plist; + QVERIFY(plist.read(writePlist(QByteArrayLiteral("\n ") + Body))); + QVERIFY(!plist.hasError()); + QCOMPARE(plist[QStringLiteral("CFBundleName")].toString(), QStringLiteral("Android KTX")); +} + +void PlistTest::testByteOrderMark() +{ + Plist plist; + QVERIFY(plist.read(writePlist(QByteArrayLiteral("\xEF\xBB\xBF") + Body))); + QVERIFY(!plist.hasError()); + QCOMPARE(plist[QStringLiteral("CFBundleName")].toString(), QStringLiteral("Android KTX")); +} + +void PlistTest::testMalformedContent() +{ + const QString path = writePlist(QByteArrayLiteral("CFBundleName")); + QVERIFY(!path.isEmpty()); + + Plist plist; + QVERIFY(!plist.read(path)); + QVERIFY(plist.hasError()); +} + +void PlistTest::testMissingFile() +{ + Plist plist; + QVERIFY(!plist.read(m_dir.filePath(QStringLiteral("Missing.plist")))); + QVERIFY(plist.hasError()); +} + +QTEST_GUILESS_MAIN(PlistTest) +#include "plist_test.moc"