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();
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\" "
"<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"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. */
QByteArray input("<!DOCTYPE html SYSTEM \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">"
"<html xmlns=\"http://www.w3.org/1999/xhtml\" xml:lang=\"en\" lang=\"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());
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&#160;&#160;&#160;&#160;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>&#160;&#160;&#160;&#160;</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");
QTest::newRow("default ns") << QByteArray("<element xmlns='http://example.com/'/>")
<< QString("element")
<< QString("element")
<< QString("http://example.com/");
QTest::newRow("with prefix") << QByteArray("<p:element xmlns:p='http://example.com/'/>")
<< QString("p:element")
<< QString("element")
<< QString("http://example.com/");
}
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));
QCOMPARE(handler.recursionCount, 2);
}
}
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 += "<n:a xmlns:n='http://example.com/nesting'>";
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&amp;<!-- 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.");
}
}
QTEST_MAIN(tst_QXmlSimpleReader)
#include "tst_qxmlsimplereader.moc"