Revision control
Copy as Markdown
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QDomDocument>
#include <qthread.h>
#include <qtcpserver.h>
#include <qtcpsocket.h>
#include <QtTest/QtTest>
#include <QtCore/qatomic.h>
#include <QtCore/qsemaphore.h>
#include <qfile.h>
#include <qstring.h>
#include <qdir.h>
#include <qbuffer.h>
#include "parser/parser.h"
#include <limits>
static const char *const inputString = "<!DOCTYPE inferno [<!ELEMENT inferno (circle+)><!ELEMENT circle (#PCDATA)>]><inferno><circle /><circle /></inferno>";
static const char *const refString = "setDocumentLocator(locator={columnNumber=1, lineNumber=1})\nstartDocument()\nstartDTD(name=\"inferno\", publicId=\"\", systemId=\"\")\nendDTD()\nstartElement(namespaceURI=\"\", localName=\"inferno\", qName=\"inferno\", atts=[])\nstartElement(namespaceURI=\"\", localName=\"circle\", qName=\"circle\", atts=[])\nendElement(namespaceURI=\"\", localName=\"circle\", qName=\"circle\")\nstartElement(namespaceURI=\"\", localName=\"circle\", qName=\"circle\", atts=[])\nendElement(namespaceURI=\"\", localName=\"circle\", qName=\"circle\")\nendElement(namespaceURI=\"\", localName=\"inferno\", qName=\"inferno\")\nendDocument()\n";
#define TEST_PORT 1088
class XmlServer : public QThread
{
Q_OBJECT
public:
XmlServer(QObject *parent = 0) : QThread(parent) {}
QSemaphore threadStarted;
bool listening = false;
QAtomicInt quitSoon;
protected:
void run() override;
};
#define CHUNK_SIZE 2048
void XmlServer::run()
{
QTcpServer srv;
listening = srv.listen(QHostAddress::Any, TEST_PORT);
threadStarted.release();
if (!listening) {
qWarning() << "Failed to listen on" << TEST_PORT << srv.errorString();
return;
}
for (;;) {
srv.waitForNewConnection(100);
if (QTcpSocket *sock = srv.nextPendingConnection()) {
QByteArray fileName;
for (;;) {
char c;
if (sock->getChar(&c)) {
if (c == '\n')
break;
fileName.append(c);
} else {
if (!sock->waitForReadyRead(-1))
break;
}
}
QFile file(QString::fromLocal8Bit(fileName));
if (!file.open(QIODevice::ReadOnly)) {
qWarning() << "XmlServer::run(): could not open" << fileName;
sock->abort();
delete sock;
continue;
}
QByteArray data = file.readAll();
for (int i = 0; i < data.size();) {
int cnt = qMin(CHUNK_SIZE, data.size() - i);
sock->write(data.constData() + i, cnt);
i += cnt;
sock->flush();
QTest::qSleep(1);
if (quitSoon.loadAcquire()) {
sock->abort();
break;
}
}
sock->disconnectFromHost();
delete sock;
}
if (quitSoon.loadAcquire())
break;
}
srv.close();
}
class tst_QXmlSimpleReader : public QObject
{
Q_OBJECT
public:
tst_QXmlSimpleReader();
~tst_QXmlSimpleReader();
private slots:
void initTestCase();
void testGoodXmlFile();
void testGoodXmlFile_data();
void testBadXmlFile();
void testBadXmlFile_data();
void testIncrementalParsing();
void testIncrementalParsing_data();
void setDataQString();
void inputFromQIODevice();
void inputFromString();
void inputFromSocket_data();
void inputFromSocket();
void idsInParseException1();
void idsInParseException2();
void preserveCharacterReferences() const;
void reportNamespace() const;
void reportNamespace_data() const;
void roundtripWithNamespaces() const;
void dtdRecursionLimit();
void deepElementNesting_data();
void deepElementNesting();
void deepElementNestingIncremental_data();
void deepElementNestingIncremental();
void valueLongerThanIntMaxDoesNotCrash();
void entityExpansionLimit_data();
void entityExpansionLimit();
void entityExpansionLimitEntityResolver_data();
void entityExpansionLimitEntityResolver();
void entityExpansionProperty_data();
void entityExpansionProperty();
private:
static QDomDocument fromByteArray(const QString &title, const QByteArray &ba, bool *ok);
XmlServer *server;
QString prefix;
};
tst_QXmlSimpleReader::tst_QXmlSimpleReader() : server(new XmlServer(this))
{
server->start();
}
tst_QXmlSimpleReader::~tst_QXmlSimpleReader()
{
server->quitSoon.storeRelease(1);
server->wait();
}
class MyErrorHandler : public QXmlErrorHandler
{
public:
QString publicId;
QString systemId;
bool error(const QXmlParseException &) override
{
return false;
}
QString errorString() const override
{
return QString();
}
bool fatalError(const QXmlParseException &exception) override
{
publicId = exception.publicId();
systemId = exception.systemId();
return true;
}
bool warning(const QXmlParseException &) override
{
return true;
}
};
void tst_QXmlSimpleReader::initTestCase()
{
prefix = QFileInfo(QFINDTESTDATA("xmldocs")).absolutePath();
if (prefix.isEmpty())
QFAIL("Cannot find xmldocs testdata!");
QDir::setCurrent(prefix);
}
void tst_QXmlSimpleReader::idsInParseException1()
{
MyErrorHandler handler;
QXmlSimpleReader reader;
reader.setErrorHandler(&handler);
/* A non-wellformed XML document with PUBLIC and SYSTEM. */
QByteArray input("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" "
"<head>"
"<a/><a/><a/>"
"<head/>");
QBuffer buff(&input);
QXmlInputSource source(&buff);
/* Yes, parsing should be reported as a failure. */
QVERIFY(!reader.parse(source));
QCOMPARE(handler.publicId, QString::fromLatin1("-//W3C//DTD XHTML 1.0 Strict//EN"));
QCOMPARE(handler.systemId, QString::fromLatin1("http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"));
}
void tst_QXmlSimpleReader::idsInParseException2()
{
MyErrorHandler handler;
QXmlSimpleReader reader;
reader.setErrorHandler(&handler);
/* A non-wellformed XML document with only SYSTEM. */
"<head>"
"<a/><a/><a/>"
"<head/>");
QBuffer buff(&input);
QXmlInputSource source(&buff);
/* Yes, parsing should be reported as a failure. */
QVERIFY(!reader.parse(source));
QCOMPARE(handler.publicId, QString());
QCOMPARE(handler.systemId, QString::fromLatin1("http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"));
}
static QStringList findXmlFiles(QString dir_name)
{
QStringList result;
dir_name = QFINDTESTDATA(dir_name);
QDir dir(dir_name);
QFileInfoList file_list = dir.entryInfoList(QStringList("*.xml"), QDir::Files, QDir::Name);
QFileInfoList::const_iterator it = file_list.begin();
for (; it != file_list.end(); ++it) {
const QFileInfo &file_info = *it;
result.append(file_info.filePath());
}
return result;
}
void tst_QXmlSimpleReader::testGoodXmlFile_data()
{
const char * const good_data_dirs[] = {
"xmldocs/valid/sa",
"xmldocs/valid/not-sa",
"xmldocs/valid/ext-sa",
0
};
const char * const *d = good_data_dirs;
QStringList good_file_list;
for (; *d != 0; ++d)
good_file_list += findXmlFiles(*d);
QTest::addColumn<QString>("file_name");
QStringList::const_iterator it = good_file_list.begin();
for (; it != good_file_list.end(); ++it)
QTest::newRow(qPrintable(*it)) << *it;
}
void tst_QXmlSimpleReader::testGoodXmlFile()
{
QFETCH(QString, file_name);
QFile file(file_name);
QVERIFY(file.open(QIODevice::ReadOnly));
QString content = file.readAll();
file.close();
QVERIFY(file.open(QIODevice::ReadOnly));
Parser parser;
QVERIFY(parser.parseFile(&file));
QFile ref_file(file_name + ".ref");
QVERIFY(ref_file.open(QIODevice::ReadOnly | QIODevice::Text));
QByteArray data = ref_file.readAll();
QString ref_file_contents = QString::fromUtf8(data.constData(), data.size());
QCOMPARE(parser.result(), ref_file_contents);
}
void tst_QXmlSimpleReader::testBadXmlFile_data()
{
const char * const bad_data_dirs[] = {
"xmldocs/not-wf/sa",
0
};
const char * const *d = bad_data_dirs;
QStringList bad_file_list;
for (; *d != 0; ++d)
bad_file_list += findXmlFiles(*d);
QTest::addColumn<QString>("file_name");
QStringList::const_iterator it = bad_file_list.begin();
for (; it != bad_file_list.end(); ++it)
QTest::newRow(qPrintable(*it)) << *it;
}
void tst_QXmlSimpleReader::testBadXmlFile()
{
QFETCH(QString, file_name);
QFile file(file_name);
QVERIFY(file.open(QIODevice::ReadOnly));
Parser parser;
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/030.xml").toLocal8Bit().constData(), "a form feed character is not accepted in XML", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/031.xml").toLocal8Bit().constData(), "a form feed character is not accepted in a processing instruction", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/032.xml").toLocal8Bit().constData(), "a form feed character is not accepted in a comment", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/033.xml").toLocal8Bit().constData(), "overlong sequence - small latin letter d should be rejected", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/038.xml").toLocal8Bit().constData(), "attribute x redefined; should be rejected", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/072.xml").toLocal8Bit().constData(), "entity foo not defined", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/073.xml").toLocal8Bit().constData(), "entity f not defined", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/074.xml").toLocal8Bit().constData(), "entity e is not well-formed (</foo><foo>)", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/076.xml").toLocal8Bit().constData(), "entity foo is not defined", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/077.xml").toLocal8Bit().constData(), "entity bar is not defined within the definition of entity foo", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/078.xml").toLocal8Bit().constData(), "entity foo not defined", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/085.xml").toLocal8Bit().constData(), "Unfinished Public or System Id", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/086.xml").toLocal8Bit().constData(), "Unfinished Public or System Id", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/087.xml").toLocal8Bit().constData(), "Unfinished Public or System Id", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/101.xml").toLocal8Bit().constData(), "Invalid XML encoding name (space before utf-8)", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/102.xml").toLocal8Bit().constData(), "Invalid version specification (1.0 followed by space)", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/104.xml").toLocal8Bit().constData(), "Premature end of data in tag foo", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/116.xml").toLocal8Bit().constData(), "Invalid decimal value", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/117.xml").toLocal8Bit().constData(), "No name", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/119.xml").toLocal8Bit().constData(), "No name", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/122.xml").toLocal8Bit().constData(), "; expected in declaration of element", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/132.xml").toLocal8Bit().constData(), "; expected in declaration of element", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/142.xml").toLocal8Bit().constData(), "Invalid value '0'", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/143.xml").toLocal8Bit().constData(), "Invalid value '31'", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/144.xml").toLocal8Bit().constData(), "noncharacter code 0xFFFF should be rejected", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/145.xml").toLocal8Bit().constData(), "surrogate code point 0xD800 should be rejected", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/146.xml").toLocal8Bit().constData(), "code point out-of-range 0x110000 (must be < 0x10FFFE)", Abort);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/160.xml").toLocal8Bit().constData(), "Parameter references forbidden in internal subset", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/162.xml").toLocal8Bit().constData(), "Parameter references forbidden in internal subset", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/168.xml").toLocal8Bit().constData(), "Surrogate code point 0xEDA080 should be rejected", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/169.xml").toLocal8Bit().constData(), "Surrogate code point 0xEDB080 should be rejected", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/170.xml").toLocal8Bit().constData(), "Code point 0xF7808080 should be rejected", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/180.xml").toLocal8Bit().constData(), "Entity e is not defined", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/181.xml").toLocal8Bit().constData(), "Unregistered error message", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/182.xml").toLocal8Bit().constData(), "Comment not terminated", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/185.xml").toLocal8Bit().constData(), "Entity e not defined", Continue);
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/186.xml").toLocal8Bit().constData(), "Attributes constructs error", Continue);
QVERIFY(!parser.parseFile(&file));
QFile ref_file(file_name + ".ref");
QVERIFY(ref_file.open(QIODevice::ReadOnly | QIODevice::Text));
QString ref_file_contents = QString::fromUtf8(ref_file.readAll());
QEXPECT_FAIL(QFINDTESTDATA("xmldocs/not-wf/sa/145.xml").toLocal8Bit().constData(), "Surrogate code point 0xD800 should be rejected", Continue);
QCOMPARE(parser.result(), ref_file_contents);
}
void tst_QXmlSimpleReader::testIncrementalParsing_data()
{
QTest::addColumn<QString>("file_name");
QTest::addColumn<int>("chunkSize");
const char * const good_data_dirs[] = {
"xmldocs/valid/sa",
"xmldocs/valid/not-sa",
"xmldocs/valid/ext-sa",
0
};
const char * const *d = good_data_dirs;
QStringList good_file_list;
for (; *d != 0; ++d)
good_file_list += findXmlFiles(*d);
for (int i=1; i<10; ++i) {
QStringList::const_iterator it = good_file_list.begin();
const QString skip49 = QFINDTESTDATA("xmldocs/valid/sa/049.xml");
const QString skip50 = QFINDTESTDATA("xmldocs/valid/sa/050.xml");
const QString skip51 = QFINDTESTDATA("xmldocs/valid/sa/051.xml");
const QString skip52 = QFINDTESTDATA("xmldocs/valid/sa/052.xml");
const QString skip89 = QFINDTESTDATA("xmldocs/valid/sa/089.xml");
for (; it != good_file_list.end(); ++it) {
if ( *it == skip89 )
continue;// TODO: fails at the moment -- don't bother
if ( i==1 && (
*it == skip49 ||
*it == skip50 ||
*it == skip51 ||
*it == skip52 ) ) {
continue; // TODO: fails at the moment -- don't bother
}
QTest::addRow("%s %d", qPrintable(*it), i) << *it << i;
}
}
}
void tst_QXmlSimpleReader::testIncrementalParsing()
{
QFETCH(QString, file_name);
QFETCH(int, chunkSize);
QFile file(file_name);
QVERIFY(file.open(QIODevice::ReadOnly));
Parser parser;
QXmlInputSource source;
bool first = true;
while (!file.atEnd()) {
source.setData(file.read(chunkSize));
if (first) {
QVERIFY(parser.parse(&source, true));
first = false;
} else {
QVERIFY(parser.parseContinue());
}
}
// detect end of document
QVERIFY(parser.parseContinue());
// parsing should fail after the end of the document was reached
QVERIFY(!parser.parseContinue());
QFile ref_file(file_name + ".ref");
QVERIFY(ref_file.open(QIODevice::ReadOnly | QIODevice::Text));
QString ref_file_contents = QString::fromUtf8(ref_file.readAll());
QCOMPARE(parser.result(), ref_file_contents);
}
void tst_QXmlSimpleReader::setDataQString()
{
QString input = inputString;
QString ref = refString;
QXmlInputSource source;
Parser parser;
source.setData(input);
QVERIFY(parser.parse(&source,false));
QBuffer resultBuffer;
resultBuffer.setData(parser.result().toLatin1());
QBuffer refBuffer;
refBuffer.setData(ref.toLatin1());
resultBuffer.open(QIODevice::ReadOnly);
refBuffer.open(QIODevice::ReadOnly);
bool success = true;
while (resultBuffer.canReadLine()) {
if (!refBuffer.canReadLine()) {
success = false; break ;
}
if (resultBuffer.readLine().simplified() != refBuffer.readLine().simplified()) {
success = false; break ;
}
}
QVERIFY(success);
}
void tst_QXmlSimpleReader::inputFromQIODevice()
{
QBuffer inputBuffer;
inputBuffer.setData(inputString);
QXmlInputSource source(&inputBuffer);
Parser parser;
QVERIFY(parser.parse(&source,false));
QBuffer resultBuffer;
resultBuffer.setData(parser.result().toLatin1());
QBuffer refBuffer;
refBuffer.setData(refString);
resultBuffer.open(QIODevice::ReadOnly);
refBuffer.open(QIODevice::ReadOnly);
bool success = true;
while (resultBuffer.canReadLine()) {
if (!refBuffer.canReadLine()) {
success = false; break ;
}
if (resultBuffer.readLine().simplified() != refBuffer.readLine().simplified()) {
success = false; break ;
}
}
QVERIFY(success);
}
void tst_QXmlSimpleReader::inputFromString()
{
QString str = "<foo><bar>kake</bar><bar>ja</bar></foo>";
QBuffer buff;
buff.setData((char*)str.utf16(), int(str.size()*sizeof(ushort)));
QXmlInputSource input(&buff);
QXmlSimpleReader reader;
QXmlDefaultHandler handler;
reader.setContentHandler(&handler);
QVERIFY(reader.parse(&input));
}
void tst_QXmlSimpleReader::inputFromSocket_data()
{
const QStringList files = findXmlFiles(QLatin1String("encodings"));
QVERIFY(files.size() > 0);
QTest::addColumn<QString>("file_name");
for (const QString &file_name : files)
QTest::newRow(qPrintable(file_name)) << file_name;
}
void tst_QXmlSimpleReader::inputFromSocket()
{
QFETCH(QString, file_name);
if (!server->threadStarted.tryAcquire(1, 15000)) {
// If something is wrong with QThreads, it's not a reason to fail
// XML-test, we are not testing QThread here after all!
QSKIP("XmlServer/thread has not started yet");
}
// Subsequent runs should be able to acquire the semaphore.
server->threadStarted.release(1);
if (!server->listening) {
// Again, QTcpServer is not the subject of this test!
QSKIP("QTcpServer::listen failed, bailing out");
}
QTcpSocket sock;
sock.connectToHost(QHostAddress::LocalHost, TEST_PORT);
QVERIFY2(sock.waitForConnected(),
qPrintable(QStringLiteral("Cannot connect on port ") + QString::number(TEST_PORT)
+ QStringLiteral(": ") + sock.errorString()));
sock.write(file_name.toLocal8Bit() + "\n");
QVERIFY(sock.waitForBytesWritten());
QXmlInputSource input(&sock);
QXmlSimpleReader reader;
QXmlDefaultHandler handler;
reader.setContentHandler(&handler);
QVERIFY(reader.parse(&input));
// qDebug() << "tst_QXmlSimpleReader::inputFromSocket(): success" << file_name;
}
void tst_QXmlSimpleReader::preserveCharacterReferences() const
{
class Handler : public QXmlDefaultHandler
{
public:
bool characters(const QString &chars) override
{
received = chars;
return true;
}
QString received;
};
{
QByteArray input("<e>A    A</e>");
QBuffer buff(&input);
QXmlInputSource source(&buff);
Handler h;
QXmlSimpleReader reader;
reader.setContentHandler(&h);
QVERIFY(reader.parse(&source, false));
QCOMPARE(h.received, QLatin1Char('A') + QString(4, QChar(160)) + QLatin1Char('A'));
}
{
QByteArray input("<e>    </e>");
QBuffer buff(&input);
QXmlInputSource source(&buff);
Handler h;
QXmlSimpleReader reader;
reader.setContentHandler(&h);
QVERIFY(reader.parse(&source, false));
QCOMPARE(h.received, QString(4, QChar(160)));
}
}
void tst_QXmlSimpleReader::reportNamespace() const
{
class Handler : public QXmlDefaultHandler
{
public:
bool startElement(const QString &namespaceURI,
const QString &localName,
const QString &qName,
const QXmlAttributes &) override
{
startNamespaceURI = namespaceURI;
startLocalName = localName;
startQName = qName;
return true;
}
bool endElement(const QString &namespaceURI,
const QString &localName,
const QString &qName) override
{
endNamespaceURI = namespaceURI;
endLocalName = localName;
endQName = qName;
return true;
}
QString startLocalName;
QString startQName;
QString startNamespaceURI;
QString endLocalName;
QString endQName;
QString endNamespaceURI;
};
QXmlSimpleReader reader;
Handler handler;
reader.setContentHandler(&handler);
QFETCH(QByteArray, input);
QBuffer buffer(&input);
QVERIFY(buffer.open(QIODevice::ReadOnly));
QXmlInputSource source(&buffer);
QVERIFY(reader.parse(source));
QFETCH(QString, expectedQName);
QFETCH(QString, expectedLocalName);
QFETCH(QString, expectedNamespace);
QCOMPARE(handler.startNamespaceURI, expectedNamespace);
QCOMPARE(handler.startLocalName, expectedLocalName);
QCOMPARE(handler.startQName, expectedQName);
QCOMPARE(handler.endNamespaceURI, expectedNamespace);
QCOMPARE(handler.endLocalName, expectedLocalName);
QCOMPARE(handler.endQName, expectedQName);
}
void tst_QXmlSimpleReader::reportNamespace_data() const
{
QTest::addColumn<QByteArray>("input");
QTest::addColumn<QString>("expectedQName");
QTest::addColumn<QString>("expectedLocalName");
QTest::addColumn<QString>("expectedNamespace");
<< QString("element")
<< QString("element")
<< QString("p:element")
<< QString("element")
}
QDomDocument tst_QXmlSimpleReader::fromByteArray(const QString &title, const QByteArray &ba, bool *ok)
{
QDomDocument doc(title);
*ok = bool(doc.setContent(ba, QDomDocument::ParseOption::UseNamespaceProcessing));
return doc;
}
void tst_QXmlSimpleReader::roundtripWithNamespaces() const
{
const char *const expected = "<element b:attr=\"value\" xmlns:a=\"http://www.example.com/A\" xmlns:b=\"http://www.example.com/B\" />\n";
bool ok;
{
const char *const xml = "<element xmlns:b=\"http://www.example.com/B\" b:attr=\"value\" xmlns:a=\"http://www.example.com/A\"/>";
const QDomDocument one(fromByteArray("document", xml, &ok));
QVERIFY(ok);
const QDomDocument two(fromByteArray("document2", one.toByteArray(2), &ok));
QVERIFY(ok);
QEXPECT_FAIL("", "Known problem, see 154573. The fix happens to break uic.", Abort);
QCOMPARE(expected, one.toByteArray().constData());
QCOMPARE(one.toByteArray(2).constData(), two.toByteArray(2).constData());
QCOMPARE(two.toByteArray(2).constData(), two.toByteArray(2).constData());
}
{
const char *const xml = "<element b:attr=\"value\" xmlns:b=\"http://www.example.com/B\" xmlns:a=\"http://www.example.com/A\"/>";
const QDomDocument one(fromByteArray("document", xml, &ok));
QVERIFY(ok);
const QDomDocument two(fromByteArray("document2", one.toByteArray(2), &ok));
QVERIFY(ok);
QCOMPARE(expected, one.toByteArray().constData());
QCOMPARE(one.toByteArray(2).constData(), two.toByteArray(2).constData());
QCOMPARE(two.toByteArray(2).constData(), two.toByteArray(2).constData());
}
}
class TestHandler : public QXmlDefaultHandler
{
public:
TestHandler() :
recursionCount(0)
{
}
bool internalEntityDecl(const QString &name, const QString &value) override
{
++recursionCount;
return QXmlDefaultHandler::internalEntityDecl(name, value);
}
int recursionCount;
};
void tst_QXmlSimpleReader::dtdRecursionLimit()
{
QFile file(QFINDTESTDATA("xmldocs/2-levels-nested-dtd.xml"));
QVERIFY(file.open(QIODevice::ReadOnly));
QXmlSimpleReader xmlReader;
{
QXmlInputSource source(&file);
TestHandler handler;
xmlReader.setDeclHandler(&handler);
xmlReader.setErrorHandler(&handler);
QVERIFY(!xmlReader.parse(&source));
}
file.close();
file.setFileName(QFINDTESTDATA("xmldocs/1-levels-nested-dtd.xml"));
QVERIFY(file.open(QIODevice::ReadOnly));
{
QXmlInputSource source(&file);
TestHandler handler;
xmlReader.setDeclHandler(&handler);
xmlReader.setErrorHandler(&handler);
QVERIFY(!xmlReader.parse(&source));
// The error wasn't because of the recursion limit being reached,
// it was because the document is not valid.
QVERIFY(handler.recursionCount < 2);
}
file.close();
file.setFileName(QFINDTESTDATA("xmldocs/internal-entity-polynomial-attribute.xml"));
QVERIFY(file.open(QIODevice::ReadOnly));
{
QXmlInputSource source(&file);
TestHandler handler;
xmlReader.setDeclHandler(&handler);
xmlReader.setErrorHandler(&handler);
QVERIFY(!xmlReader.parse(&source));
// The entity sizes are checked when the entities are expanded, so all
// four entities are declared before the expansion in the attribute fails.
QCOMPARE(handler.recursionCount, 4);
}
}
class DepthMeasureHandler : public QXmlDefaultHandler
{
public:
bool startElement(const QString &, const QString &, const QString &,
const QXmlAttributes &) override
{
maxDepth = qMax(maxDepth, ++depth);
return true;
}
bool endElement(const QString &, const QString &, const QString &) override
{
--depth;
return true;
}
bool fatalError(const QXmlParseException &exception) override
{
errorMessage = exception.message();
return false;
}
int depth = 0;
int maxDepth = 0;
QString errorMessage;
};
void tst_QXmlSimpleReader::deepElementNesting_data()
{
QTest::addColumn<QByteArray>("xml");
QTest::addColumn<int>("expectedDepth");
constexpr int depth = 200000;
{
QByteArray xml;
xml.reserve(depth * 7);
for (int i = 0; i < depth; ++i)
xml += "<a>";
for (int i = 0; i < depth; ++i)
xml += "</a>";
QTest::newRow("closed tags") << xml << depth;
}
{
QByteArray xml;
xml.reserve(xml.size() + depth * 11);
for (int i = 0; i < depth - 1; ++i) // the outermost element is a level too
xml += "<n:a>";
for (int i = 0; i < depth; ++i)
xml += "</n:a>";
QTest::newRow("namespace prefixed tags") << xml << depth;
}
{
constexpr char level[] = "<a>text&<!-- comment --><?pi data?><![CDATA[<]]>";
qsizetype levelLen = qsizetype(strlen(level));
QByteArray xml;
xml.reserve(depth * (levelLen + 4));
for (int i = 0; i < depth; ++i)
xml += level;
for (int i = 0; i < depth; ++i)
xml += "</a>";
QTest::newRow("mixed content") << xml << depth;
}
{
QByteArray xml;
xml += "<!DOCTYPE a [<!ELEMENT a ";
xml.reserve(xml.size() + depth * 2);
xml.resize(xml.size() + depth, '(');
xml += 'b';
xml.resize(xml.size() + depth, ')');
xml += ">]><a/>";
QTest::newRow("DTD nesting") << xml << 1;
}
}
void tst_QXmlSimpleReader::deepElementNesting()
{
QFETCH(QByteArray, xml);
QFETCH(const int, expectedDepth);
enum class CallType
{
Ref,
Pointer,
TwoArg,
};
constexpr CallType types[] = { CallType::Ref, CallType::Pointer, CallType::TwoArg };
for (CallType type : types) {
auto guard = qScopeGuard([type] {
qDebug("Failed while handling %s parse() overload",
type == CallType::Ref ? "const ref" :
type == CallType::Pointer ? "pointer" :
/* else */ "two arg");
});
QBuffer buffer(&xml);
QVERIFY(buffer.open(QIODevice::ReadOnly));
QXmlInputSource source(&buffer);
DepthMeasureHandler handler;
QXmlSimpleReader reader;
reader.setContentHandler(&handler);
reader.setErrorHandler(&handler);
reader.setLexicalHandler(&handler);
switch (type) {
case CallType::Ref:
QVERIFY(reader.parse(source));
break;
case CallType::Pointer:
QVERIFY(reader.parse(&source));
break;
case CallType::TwoArg:
QVERIFY(reader.parse(&source, false));
break;
}
QVERIFY(handler.errorMessage.isEmpty());
QCOMPARE(handler.maxDepth, expectedDepth);
QCOMPARE(handler.depth, 0);
guard.dismiss();
}
}
void tst_QXmlSimpleReader::deepElementNestingIncremental_data()
{
deepElementNesting_data();
}
void tst_QXmlSimpleReader::deepElementNestingIncremental()
{
QFETCH(QByteArray, xml);
QFETCH(const int, expectedDepth);
DepthMeasureHandler handler;
QXmlSimpleReader reader;
reader.setContentHandler(&handler);
reader.setErrorHandler(&handler);
reader.setLexicalHandler(&handler);
const qsizetype chunkSize = xml.size() / 10;
QVERIFY(chunkSize > 0);
QXmlInputSource source;
bool first = true;
for (qsizetype pos = 0; pos < xml.size(); pos += chunkSize) {
source.setData(xml.mid(pos, chunkSize));
// In incremental mode running out of data is reported as success, so a
// false here would be a real parse error.
if (first) {
QVERIFY(reader.parse(&source, true));
first = false;
} else {
QVERIFY(reader.parseContinue());
}
}
// detect end of document
QVERIFY(reader.parseContinue());
QVERIFY(handler.errorMessage.isEmpty());
QCOMPARE(handler.maxDepth, expectedDepth);
QCOMPARE(handler.depth, 0);
}
void tst_QXmlSimpleReader::valueLongerThanIntMaxDoesNotCrash()
{
if constexpr (QT_POINTER_SIZE == 4)
QSKIP("An allocated QString does not fit into a 32 bit address space.");
if (qgetenv("QTEST_ENVIRONMENT").split(' ').contains("ci"))
QSKIP("Too slow and consumes at least 4 GiB of memory - skipping on CI.");
// Serves prefix + runLength copies of runChar + suffix in chunks to avoid
// storing a multi-Gb input
class RepeatedRunDevice : public QIODevice
{
public:
RepeatedRunDevice(const QByteArray &prefix, qint64 runLength, const QByteArray &suffix,
char runChar = 'a')
: m_prefix(prefix), m_suffix(suffix), m_runLength(runLength), m_runChar(runChar)
{
}
qint64 size() const override { return m_prefix.size() + m_runLength + m_suffix.size(); }
protected:
qint64 readData(char *data, qint64 maxSize) override
{
const qint64 runStart = m_prefix.size();
const qint64 suffixStart = runStart + m_runLength;
qint64 offset = pos();
qint64 written = 0;
while (written < maxSize && offset < size()) {
const char *source = nullptr;
qint64 available = 0;
if (offset < runStart) {
source = m_prefix.constData() + offset;
available = runStart - offset;
} else if (offset < suffixStart) {
available = suffixStart - offset;
} else {
source = m_suffix.constData() + (offset - suffixStart);
available = size() - offset;
}
const qint64 chunk = qMin(available, maxSize - written);
if (source)
memcpy(data + written, source, chunk);
else
memset(data + written, m_runChar, chunk);
written += chunk;
offset += chunk;
}
return written;
}
qint64 writeData(const char *, qint64) override { return -1; }
private:
QByteArray m_prefix;
QByteArray m_suffix;
qint64 m_runLength;
char m_runChar;
};
class CharacterCountingHandler : public QXmlDefaultHandler
{
public:
bool characters(const QString &ch) override
{
characterCount += ch.size();
return true;
}
bool fatalError(const QXmlParseException &exception) override
{
errorMessage = exception.message();
return false;
}
qsizetype characterCount = 0;
QString errorMessage;
};
// The buffer is flushed every 256 characters, so a handful of characters
// past INT_MAX is enough to be sure the boundary is crossed.
constexpr auto runLength = std::numeric_limits<int>::max() + qint64{10};
CharacterCountingHandler handler;
QXmlSimpleReader reader;
reader.setContentHandler(&handler);
reader.setErrorHandler(&handler);
QT_TRY {
RepeatedRunDevice device("<d>", runLength, "</d>");
QVERIFY(device.open(QIODevice::ReadOnly));
QXmlInputSource source(&device);
QVERIFY2(reader.parse(source), qPrintable(handler.errorMessage));
QCOMPARE(handler.characterCount, runLength);
} QT_CATCH (const std::bad_alloc &) {
QSKIP("Not enough memory to accumulate INT_MAX characters.");
}
}
constexpr qsizetype EntityCharacterLimit = 4096;
void tst_QXmlSimpleReader::entityExpansionLimit_data()
{
QTest::addColumn<QByteArray>("xml");
constexpr qsizetype OversizedLiteralSize = EntityCharacterLimit + 1;
constexpr qsizetype ExpandedSize = 16 * EntityCharacterLimit;
// Literal entity in content is larger than the expansion limit
{
const QByteArray xml =
"<!DOCTYPE d [<!ENTITY e '" + QByteArray(OversizedLiteralSize, 'A') + "'>]>"
"<d>&e;</d>";
QTest::newRow("literal_entity_in_content") << xml;
}
// Literal entity in attribute value is larger than the expansion limit
{
const QByteArray xml =
"<!DOCTYPE d [<!ENTITY e '" + QByteArray(OversizedLiteralSize, 'A') + "'>]>"
"<d a='&e;'/>";
QTest::newRow("literal_entity_in_attribute_value") << xml;
}
constexpr qsizetype NestedReferences = 2;
// e2 references e1 multiple times. The total expanded size of e2 exceeds the limit.
// e2 is used in the content.
{
const QByteArray xml =
"<!DOCTYPE d [<!ENTITY e1 '" + QByteArray(EntityCharacterLimit, 'A') + "'>"
"<!ENTITY e2 '" + QByteArray("&e1;").repeated(NestedReferences) + "'>]>"
"<d>&e2;</d>";
QTest::newRow("two_declarations_in_content") << xml;
}
// e2 references e1 multiple times. The total expanded size of e2 exceeds the limit.
// e2 is used in the attribute value.
{
const QByteArray xml =
"<!DOCTYPE d [<!ENTITY e1 '" + QByteArray(EntityCharacterLimit, 'A') + "'>"
"<!ENTITY e2 '" + QByteArray("&e1;").repeated(NestedReferences) + "'>]>"
"<d a='&e2;'/>";
QTest::newRow("two_declarations_in_attribute_value") << xml;
}
constexpr qsizetype ChainReferences = 2;
constexpr qsizetype ChainLiteralSize = EntityCharacterLimit / 4;
// The entities are declared in the reverse order. Verifies that we properly
// re-evaluate the expanded sizes once new entities are parsed.
{
const QByteArray xml =
"<!DOCTYPE d ["
"<!ENTITY e4 '" + QByteArray("&e3;").repeated(ChainReferences) + "'>"
"<!ENTITY e3 '" + QByteArray("&e2;").repeated(ChainReferences) + "'>"
"<!ENTITY e2 '" + QByteArray("&e1;").repeated(ChainReferences) + "'>"
"<!ENTITY e1 '" + QByteArray(ChainLiteralSize, 'A') + "'>"
"]><d>&e4;</d>";
QTest::newRow("reverse_declaration_order") << xml;
}
// Parameter entity with oversized literal size
{
const QByteArray xml =
"<!DOCTYPE d [<!ENTITY % a '" + QByteArray(OversizedLiteralSize, 'A') + "'>"
"<!ENTITY % b '%a;'>"
"]><d/>";
QTest::newRow("parameter_entity_in_DTD") << xml;
}
// Parameter entity b references parameter entity a multiple times. The total
// expanded size of b exceeds the limit.
{
const QByteArray xml =
"<!DOCTYPE d [<!ENTITY % a '" + QByteArray(EntityCharacterLimit, 'A') + "'>"
"<!ENTITY % b '"
+ QByteArray("%a;").repeated(ExpandedSize / EntityCharacterLimit)
+ "'>]><d/>";
QTest::newRow("parameter_entity_run_in_DTD") << xml;
}
// Same as above, but the parameter entity b expands into a huge string in
// memory. Needed to test that we properly limit the expansion while
// substituting parameter entites, rather than check the full size of expanded
// string afterwards.
{
constexpr qsizetype RunawayReferences = 1'000'000;
const QByteArray xml =
"<!DOCTYPE d [<!ENTITY % a '" + QByteArray(EntityCharacterLimit, 'A') + "'>"
"<!ENTITY % b '"
+ QByteArray("%a;").repeated(RunawayReferences)
+ "'>]><d/>";
QTest::newRow("parameter_entity_run_in_DTD_huge_expansion") << xml;
}
// Entity e references parameter entity a multiple times. The total expanded
// size of e exceeds the limit, and parameter entity a itself exceeds the limit.
{
const QByteArray xml =
"<!DOCTYPE d [<!ENTITY % a '" + QByteArray(OversizedLiteralSize, 'A') + "'>"
"<!ENTITY e '"
+ QByteArray("%a;").repeated(ExpandedSize / OversizedLiteralSize)
+"'>]><d/>";
QTest::newRow("parameter_entity_in_entity_value") << xml;
}
// The parameter entity a has references to general entity e1 in its value.
// The expansion limit is exceeded only when the parameter entity a is used
// in another general entity e2.
{
const QByteArray xml =
"<!DOCTYPE d [<!ENTITY e1 '" + QByteArray(EntityCharacterLimit, 'A') + "'>"
"<!ENTITY % a '" + QByteArray("&e1;").repeated(NestedReferences) + "'>"
"<!ENTITY e2 '%a;'>]>"
"<d>&e2;</d>";
QTest::newRow("entity_refs_in_parameter_entity") << xml;
}
// The character reference "%" is expanded to '%' when the value of the
// parameter entity b is parsed, so b is stored as "%a;%a;", and fits into
// the limit. When b is included in the DTD, its value is parsed again, and
// every "%a;" includes the value of a. The parameter entity a contains
// only spaces, so that it can be included in the DTD.
// The expansion limit is exceeded once b is included in the DTD.
{
const QByteArray xml =
"<!DOCTYPE d [<!ENTITY % a '" + QByteArray(EntityCharacterLimit, ' ') + "'>"
"<!ENTITY % b '" + QByteArray("%a;").repeated(NestedReferences) + "'>"
"%b;]><d/>";
QTest::newRow("parameter_entity_char_ref_in_DTD") << xml;
}
// The parameter entity p contains an ATTLIST declaration with references to
// the general entity e in the default value. The references are bypassed
// when p is declared, so p fits into the limit. When p is included in the
// DTD, the default value is parsed, and the references to e are expanded.
// The expansion limit is exceeded once p is included in the DTD.
{
const QByteArray xml =
"<!DOCTYPE d [<!ENTITY e '" + QByteArray(EntityCharacterLimit, 'A') + "'>"
"<!ENTITY % p \"<!ATTLIST d a CDATA '"
+ QByteArray("&e;").repeated(NestedReferences) + "'>\">"
"%p;]><d/>";
QTest::newRow("general_entity_in_parameter_entity_in_DTD") << xml;
}
// Expansion of standard XML entities.
// e1 contains multiple & references, but fits into EntityCharacterLimit.
// e2 is a repetition of e1 that exceeds the limit.
{
const QByteArray xml =
"<!DOCTYPE d [<!ENTITY e1 '"
+ QByteArray("&").repeated(EntityCharacterLimit / 5) + "'>"
"<!ENTITY e2 '" + QByteArray("&e1;").repeated(NestedReferences) + "'>]>"
"<d>&e2;</d>";
QTest::newRow("standard_entities") << xml;
}
// Expansion of numeric character references.
// The string "&#65;" is first transformed into "A" (when & is
// parsed as '&'), and that value is only replaced with 'A' in content.
// e1 contains multiple "&#65;" entries, but fits into
// EntityCharacterLimit.
// e2 is a repetition of e1 that exceeds the limit.
{
const QByteArray xml =
"<!DOCTYPE d [<!ENTITY e1 '"
+ QByteArray("&#65;").repeated(EntityCharacterLimit / 5) + "'>"
"<!ENTITY e2 '" + QByteArray("&e1;").repeated(NestedReferences) + "'>]>"
"<d>&e2;</d>";
QTest::newRow("numeric_character_references") << xml;
}
// Expansion with unresolved entity.
// The entity e1 contains EntityCharacterLimit valid characters + a reference
// to an unknown entity u.
// The entity e2 simply uses e1.
// e1 itself already exceeds the limit, because an unknown entity reference
// is treated as a literal.
{
const QByteArray xml =
"<!DOCTYPE d [<!ENTITY e1 '"
+ QByteArray(EntityCharacterLimit, 'A') + "&u;'>"
"<!ENTITY e2 '&e1;'>]>"
"<d>&e2;</d>";
QTest::newRow("unknown_entity") << xml;
}
// Expansion with unresolved entity in ATTLIST.
// The entity e1 contains (EntityCharacterLimit - 3) characters + a reference
// to an unknown entity u. So, it's fine on its own.
// The entity e2 references e1 twice, so it exceeds the limit.
// The ATTLIST expands entity e2.
// The test should fail during the e2 entity expansion in ATTLIST, so the
// attributeDecl() callback of the handler should never be called.
{
const QByteArray xml =
"<!DOCTYPE d [<!ENTITY e1 '"
+ QByteArray(EntityCharacterLimit - 3, 'A') + "&u;'>"
"<!ENTITY e2 '&e1;&e1;'>"
"<!ATTLIST d a CDATA '&e2;'>"
"]><d/>";
QTest::newRow("unknown_entity_in_attlist") << xml;
}
// Expansion when one of the entities contains an empty value.
// Verifies that this empty value does not invalidate the calculations.
// Entity e0 has an empty value.
// Entity e1 has EntityCharacterLimit characters + a reference to e0 - it's
// still fine.
// Entity e2 references e1 two times - it should exceed the limit
{
const QByteArray xml =
"<!DOCTYPE d [<!ENTITY e0 ''>"
"<!ENTITY e1 '" + QByteArray(EntityCharacterLimit, 'A') + "&e0;'>"
"<!ENTITY e2 '&e1;&e1;'>]>"
"<d>&e2;</d>";
QTest::newRow("empty_entity") << xml;
}
// Expansion with entity reference loop.
// Should be handled by the parser when substituting the entity in content
{
const QByteArray xml =
"<!DOCTYPE d [<!ENTITY e1 '&e2;'>"
"<!ENTITY e2 '&e1;'>]>"
"<d>&e2;</d>";
QTest::newRow("entity_loop") << xml;
}
}
void tst_QXmlSimpleReader::entityExpansionLimit()
{
// Counts every expanded character the parser hands out, wherever it hands it
// out: as character data, as an attribute value, or as the value of a declared
// entity.
class ExpansionHandler : public QXmlDefaultHandler
{
public:
bool startElement(const QString &, const QString &, const QString &,
const QXmlAttributes &atts) override
{
for (int i = 0; i < atts.length(); ++i)
expandedCharacters += atts.value(i).size();
return true;
}
bool characters(const QString &ch) override
{
expandedCharacters += ch.size();
return true;
}
bool internalEntityDecl(const QString &, const QString &value) override
{
expandedCharacters += value.size();
return true;
}
bool fatalError(const QXmlParseException &exception) override
{
errorMessage = exception.message();
return false;
}
bool attributeDecl(const QString &, const QString &, const QString &,
const QString &, const QString &) override
{
attDeclCalled = true;
return true;
}
qsizetype expandedCharacters = 0;
QString errorMessage;
bool attDeclCalled = false;
};
static constexpr qsizetype expansionSanityBound = EntityCharacterLimit * 2;
QFETCH(QByteArray, xml);
QBuffer buffer(&xml);
QVERIFY(buffer.open(QIODevice::ReadOnly));
QXmlInputSource source(&buffer);
ExpansionHandler handler;
QXmlSimpleReader reader;
reader.setContentHandler(&handler);
reader.setErrorHandler(&handler);
reader.setDeclHandler(&handler);
QVERIFY(!reader.parse(source));
QVERIFY(!handler.errorMessage.isEmpty());
QVERIFY(!handler.attDeclCalled);
QVERIFY2(handler.expandedCharacters <= expansionSanityBound,
qPrintable(QString::fromLatin1("Expanded %1 characters, which is more than the "
"sanity bound of %2.")
.arg(handler.expandedCharacters)
.arg(expansionSanityBound)));
}
void tst_QXmlSimpleReader::entityExpansionLimitEntityResolver_data()
{
QTest::addColumn<QByteArray>("xml");
QTest::addColumn<bool>("expectSuccess");
// The internal entity "outer" references the external parsed entity
// "ext", which in turn references the internal entity "e1" twice.
// The references to e1 come from the text of the external entity, so they
// were not counted as a part of the value of "outer", and the full size of
// e1 has to be added for each of them.
// The expanded size of "outer" is 5 ("&ext;") + 2 * e1.size()
const QByteArray xmlStart =
"<!DOCTYPE d ["
"<!ENTITY ext SYSTEM 'ext.ent'>"
"<!ENTITY e1 '";
const QByteArray withinLimit = QByteArray(EntityCharacterLimit / 2 - 3, 'A');
const QByteArray aboveLimit = QByteArray(EntityCharacterLimit / 2, 'A');
const QByteArray xmlEnd =
"'>"
"<!ENTITY outer '&ext;'>"
"]><d>&outer;</d>";
// 5 + 2 * (EntityCharacterLimit / 2 - 3) == EntityCharacterLimit - 1
QTest::newRow("within_limit") << xmlStart + withinLimit + xmlEnd << true;
// 5 + 2 * (EntityCharacterLimit / 2) == EntityCharacterLimit + 5.
QTest::newRow("exceeds_limit") << xmlStart + aboveLimit + xmlEnd << false;
}
void tst_QXmlSimpleReader::entityExpansionLimitEntityResolver()
{
class ExternalEntityHandler : public QXmlDefaultHandler
{
public:
explicit ExternalEntityHandler(const QByteArray &externalEntity)
: externalEntityData(externalEntity)
{
}
bool resolveEntity(const QString &, const QString &systemId,
QXmlInputSource *&ret) override
{
++resolveCount;
if (systemId == QLatin1String("ext.ent")) {
externalEntityBuffer.close();
externalEntityBuffer.setData(externalEntityData);
externalEntityBuffer.open(QIODevice::ReadOnly);
ret = new QXmlInputSource(&externalEntityBuffer);
}
return true;
}
bool characters(const QString &ch) override
{
expandedCharacters += ch.size();
return true;
}
bool fatalError(const QXmlParseException &exception) override
{
errorMessage = exception.message();
return false;
}
QByteArray externalEntityData;
QBuffer externalEntityBuffer;
int resolveCount = 0;
qsizetype expandedCharacters = 0;
QString errorMessage;
};
QFETCH(QByteArray, xml);
QFETCH(const bool, expectSuccess);
QBuffer buffer(&xml);
QVERIFY(buffer.open(QIODevice::ReadOnly));
QXmlInputSource source(&buffer);
ExternalEntityHandler handler("&e1;&e1;");
QXmlSimpleReader reader;
reader.setContentHandler(&handler);
reader.setErrorHandler(&handler);
reader.setEntityResolver(&handler);
QCOMPARE(reader.parse(source), expectSuccess);
QCOMPARE(handler.errorMessage.isEmpty(), expectSuccess);
QCOMPARE_LE(handler.expandedCharacters, EntityCharacterLimit);
QCOMPARE(handler.resolveCount, 1);
}
void tst_QXmlSimpleReader::entityExpansionProperty_data()
{
QTest::addColumn<qint64>("limit");
QTest::addColumn<qint64>("expectedLimit");
QTest::addColumn<int>("entityLength");
QTest::addColumn<bool>("wellFormed");
QTest::newRow("large_limit_valid_length") << qint64(8192) << qint64(8192) << 8000 << true;
QTest::newRow("small_limit_invalid_length") << qint64(32) << qint64(32) << 64 << false;
QTest::newRow("zero_limit_uses_default") << qint64(0) << qint64(4096) << 5000 << false;
QTest::newRow("negative_limit_uses_default") << qint64(-1) << qint64(4096) << 4000 << true;
if constexpr (sizeof(qintptr) > sizeof(int)) {
QTest::newRow("too_large_limit_uses_default")
<< qint64(std::numeric_limits<int>::max()) + 1 << qint64(4096) << 5000 << false;
}
}
void tst_QXmlSimpleReader::entityExpansionProperty()
{
QFETCH(const qint64, limit);
QFETCH(const qint64, expectedLimit);
QFETCH(const int, entityLength);
QFETCH(const bool, wellFormed);
QXmlSimpleReader reader;
QVERIFY(reader.hasProperty(QLatin1String(propName)));
bool ok = false;
void *value = reader.property(QLatin1String(propName), &ok);
QVERIFY(ok);
QCOMPARE(reinterpret_cast<qintptr>(value), qintptr(4096));
reader.setProperty(QLatin1String(propName), reinterpret_cast<void *>(qintptr(limit)));
// verify the limit
ok = false;
value = reader.property(QLatin1String(propName), &ok);
QVERIFY(ok);
QCOMPARE(reinterpret_cast<qintptr>(value), qintptr(expectedLimit));
QByteArray xml =
"<!DOCTYPE d [<!ENTITY e '" + QByteArray(entityLength, 'A') + "'>]><d>&e;</d>";
QBuffer buffer(&xml);
QVERIFY(buffer.open(QIODevice::ReadOnly));
QXmlInputSource source(&buffer);
QCOMPARE(reader.parse(source), wellFormed);
}
QTEST_MAIN(tst_QXmlSimpleReader)
#include "tst_qxmlsimplereader.moc"