QXmlStreamWriter: add option to stop writing after an error

By default, QXmlStreamWriter continues writing even after encountering
an InvalidCharacter, EncodingError or CustomError, which contradicts
expected behavior.

This change introduces property stopWritingOnError with two new
functions: setStopWritingOnError() and stopWritingOnError(), allowing
users to control whether output halts immediately after the first such
error.

[ChangeLog][QtCore][QXmlStreamWriter] Added setStopWritingOnError() and
stopWritingOnError() functions.

Fixes: QTBUG-135861
Change-Id: Ia3ba894fc5bd8c5ff3a548e2585af9d435dec9b2
Reviewed-by: Safiyyah Moosa <safiyyah.moosa@qt.io>
Reviewed-by: Edward Welbourne <edward.welbourne@qt.io>
This commit is contained in:
Magdalena Stojek 2025-05-07 14:07:40 +02:00
parent 603499d2e7
commit 263f06ae8b
3 changed files with 248 additions and 7 deletions

View File

@ -3075,12 +3075,17 @@ QStringView QXmlStreamReader::documentEncoding() const
QXmlStreamWriter always encodes XML in UTF-8.
\note If an error occurs while writing, \l hasError() will return true.
However, data that was already buffered at the time the error occurred,
or data written from within the same operation, may still be written
to the underlying device. This applies to both \l EncodingError and
user-raised \l CustomError. Applications should treat the error state
as terminal and avoid further use of the writer after an error.
If an error occurs while writing, \l hasError() will return true.
However, by default, data that was already buffered at the time the error
occurred, or data written from within the same operation, may still be
written to the underlying device. This applies to \l Error::EncodingError,
\l Error::InvalidCharacter, and user-raised \l Error::CustomError.
To avoid this and ensure no data is written after an error, use the
\l stopWritingOnError property. When this property is enabled,
the first error stops output immediately and the writer ignores all
subsequent write operations.
Applications should treat the error state as terminal and avoid further
use of the writer after an error.
The \l{QXmlStream Bookmarks Example} illustrates how to use a
stream writer to write an XML bookmark file (XBEL) that
@ -3146,6 +3151,7 @@ public:
uint autoFormatting :1;
uint didWriteStartDocument :1;
uint didWriteAnyToken :1;
uint stopWritingOnError :1;
std::string autoFormattingIndent = std::string(4, ' ');
NamespaceDeclaration emptyNamespace;
qsizetype lastNamespaceDeclaration = 1;
@ -3170,7 +3176,8 @@ QXmlStreamWriterPrivate::QXmlStreamWriterPrivate(QXmlStreamWriter *q)
: q_ptr(q), deleteDevice(false), inStartElement(false),
inEmptyElement(false), lastWasStartElement(false),
wroteSomething(false), autoFormatting(false),
didWriteStartDocument(false), didWriteAnyToken(false)
didWriteStartDocument(false), didWriteAnyToken(false),
stopWritingOnError(false)
{
}
@ -3203,6 +3210,8 @@ void QXmlStreamWriterPrivate::raiseError(QXmlStreamWriter::Error errorCode, cons
void QXmlStreamWriterPrivate::write(QAnyStringView s)
{
if (stopWritingOnError && (error != QXmlStreamWriter::Error::NoError))
return;
if (device) {
if (error == QXmlStreamWriter::Error::IOError)
return;
@ -3296,6 +3305,8 @@ void QXmlStreamWriterPrivate::writeEscaped(QAnyStringView s, bool escapeWhitespa
case u'\v':
case u'\f':
raiseError(QXmlStreamWriter::Error::InvalidCharacter);
if (stopWritingOnError)
return;
replacement = ""_L1;
Q_ASSERT(!replacement.isNull());
break;
@ -3309,6 +3320,8 @@ void QXmlStreamWriterPrivate::writeEscaped(QAnyStringView s, bool escapeWhitespa
raiseError(encodingError
? QXmlStreamWriter::Error::EncodingError
: QXmlStreamWriter::Error::InvalidCharacter);
if (stopWritingOnError)
return;
replacement = ""_L1;
Q_ASSERT(!replacement.isNull());
break;
@ -3617,6 +3630,35 @@ int QXmlStreamWriter::autoFormattingIndent() const
return indent.count(u' ') - indent.count(u'\t');
}
/*!
\property QXmlStreamWriter::stopWritingOnError
\since 6.10
\brief The option to stop writing to the device after encountering an error.
If this property is set to \c true, the writer stops writing immediately upon
encountering any error and ignores all subsequent write operations.
When this property is set to \c false, the writer may continue writing
after an error, skipping the invalid write but allowing further output.
Note that this includes \l Error::InvalidCharacter, \l Error::EncodingError,
and \l Error::CustomError. \l Error::IOError is always considered terminal
and stops writing regardless of this setting.
The default value is \c false.
*/
bool QXmlStreamWriter::stopWritingOnError() const
{
Q_D(const QXmlStreamWriter);
return d->stopWritingOnError;
}
void QXmlStreamWriter::setStopWritingOnError(bool stop)
{
Q_D(QXmlStreamWriter);
d->stopWritingOnError = stop;
}
/*!
Returns \c true if an error occurred while trying to write data.

View File

@ -377,6 +377,7 @@ class Q_CORE_EXPORT QXmlStreamWriter
{
QDOC_PROPERTY(bool autoFormatting READ autoFormatting WRITE setAutoFormatting)
QDOC_PROPERTY(int autoFormattingIndent READ autoFormattingIndent WRITE setAutoFormattingIndent)
QDOC_PROPERTY(bool stopWritingOnError READ stopWritingOnError WRITE setStopWritingOnError)
public:
QXmlStreamWriter();
explicit QXmlStreamWriter(QIODevice *device);
@ -393,6 +394,9 @@ public:
void setAutoFormattingIndent(int spacesOrTabs);
int autoFormattingIndent() const;
void setStopWritingOnError(bool stop);
bool stopWritingOnError() const;
#if QT_CORE_REMOVED_SINCE(6,5)
void writeAttribute(const QString &qualifiedName, const QString &value);
void writeAttribute(const QString &namespaceUri, const QString &name, const QString &value);

View File

@ -606,6 +606,7 @@ private slots:
void invalidStringCharacters_data() const;
void invalidStringCharacters() const;
void writerErrors() const;
void stopWritingOnError() const;
void readBack_data() const;
void readBack() const;
void roundTrip() const;
@ -2404,7 +2405,201 @@ void tst_QXmlStream::writerErrors() const
QCOMPARE(writer.error(), QXmlStreamWriter::Error::CustomError);
QCOMPARE(writer.errorString(), "Custom error"_L1);
}
}
void tst_QXmlStream::stopWritingOnError() const
{
{
// Default - stopWritingOnError(false)
QByteArray buffer;
QXmlStreamWriter writer(&buffer);
writer.writeStartDocument();
writer.writeStartElement(u"root");
writer.writeCharacters(u"Invalid \x01 character");
writer.writeTextElement(u"text", u"element");
writer.writeComment(u"A comment");
writer.writeEmptyElement(u"emptyElement");
QVERIFY(writer.hasError());
QCOMPARE(writer.error(), QXmlStreamWriter::Error::InvalidCharacter);
QVERIFY(!writer.errorString().isEmpty());
QCOMPARE(buffer, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
"<root>Invalid character<text>element</text>"
"<!--A comment--><emptyElement"_ba);
writer.writeCharacters(u"Let's raise another error!");
writer.raiseError(u"Custom error"_s);
writer.writeEndElement();
writer.writeTextElement(u"text", u"element");
QVERIFY(writer.hasError());
QCOMPARE(writer.error(), QXmlStreamWriter::Error::CustomError);
QVERIFY(!writer.errorString().isEmpty());
QCOMPARE(buffer, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
"<root>Invalid character<text>element</text>"
"<!--A comment--><emptyElement/>"
"Let's raise another error!</root>"
"<text>element</text>"_ba);
writer.writeStartElement(u"child");
writer.writeCharacters(QChar(0xDC00));
writer.writeCharacters(u"I'm still standin' better than I ever did!");
writer.writeEndElement();
writer.writeEndDocument();
QVERIFY(writer.hasError());
QCOMPARE(writer.error(), QXmlStreamWriter::Error::EncodingError);
QVERIFY(!writer.errorString().isEmpty());
QCOMPARE(buffer, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
"<root>Invalid character<text>element</text>"
"<!--A comment--><emptyElement/>"
"Let's raise another error!</root><text>element</text>"
"<child>I'm still standin' better than I ever did!</child>\n"_ba);
}
{
// Only IOError prevents further writing
QByteArray buffer;
QBuffer device(&buffer);
device.open(QIODevice::WriteOnly);
device.close();
QXmlStreamWriter writer(&device);
writer.setStopWritingOnError(false);
writer.writeStartDocument();
writer.writeStartElement(u"root");
writer.writeCharacters(u"Some characters");
QVERIFY(writer.hasError());
QCOMPARE(writer.error(), QXmlStreamWriter::Error::IOError);
QVERIFY(!writer.errorString().isEmpty());
QVERIFY(buffer.isEmpty());
}
{
// Valid input
QByteArray buffer;
QXmlStreamWriter writer(&buffer);
writer.setStopWritingOnError(true);
writer.writeStartDocument();
writer.writeStartElement(u"root");
writer.writeCharacters(u"Valid & possible to <escape> \"characters\"");
writer.writeTextElement(u"text", u"element");
writer.writeComment(u"A comment");
writer.writeEmptyElement(u"emptyElement");
writer.writeEndDocument();
QVERIFY(!writer.hasError());
QCOMPARE(writer.error(), QXmlStreamWriter::Error::NoError);
QVERIFY(writer.errorString().isEmpty());
QCOMPARE(buffer, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
"<root>Valid &amp; possible to &lt;escape&gt; &quot;characters&quot;"
"<text>element</text><!--A comment--><emptyElement/>"
"</root>\n"_ba);
}
{
// Invalid character error: invalid \x01 character
QByteArray buffer;
QXmlStreamWriter writer(&buffer);
writer.setStopWritingOnError(true);
writer.writeStartDocument();
writer.writeStartElement(u"root");
writer.writeCharacters(u"Invalid \x01 character"); // Stop writing from here
writer.writeTextElement(u"text", u"element");
writer.writeComment(u"A comment");
writer.writeEmptyElement(u"emptyElement");
writer.writeCDATA(u"CDATA");
writer.writeEntityReference(u"entityReference");
writer.writeProcessingInstruction(u"PI");
writer.writeCharacters(u"Characters");
writer.writeDTD(u"DTD");
writer.writeDefaultNamespace(u"defaultNamespace");
writer.writeNamespace(u"namespace");
writer.writeEndElement();
writer.writeEndDocument();
QVERIFY(writer.hasError());
QCOMPARE(writer.error(), QXmlStreamWriter::Error::InvalidCharacter);
QVERIFY(!writer.errorString().isEmpty());
QCOMPARE(buffer, "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>"_ba);
}
{
// Invalid character error: invalid \v character
QByteArray buffer;
QXmlStreamWriter writer(&buffer);
writer.setStopWritingOnError(true);
writer.writeStartDocument();
writer.writeStartElement(u"root");
writer.writeTextElement(u"text", u"element");
writer.writeComment(u"A comment");
writer.writeEmptyElement(u"emptyElement");
writer.writeCDATA(u"CDATA");
writer.writeEntityReference(u"entityReference");
writer.writeProcessingInstruction(u"PI");
writer.writeCharacters(u"Characters");
writer.writeCharacters(u"Invalid \v character"); // Stop writing from here
writer.writeCharacters(u"More valid characters");
writer.writeDTD(u"DTD");
writer.writeDefaultNamespace(u"defaultNamespace");
writer.writeNamespace(u"namespace");
writer.writeEndElement();
writer.writeEndDocument();
QVERIFY(writer.hasError());
QCOMPARE(writer.error(), QXmlStreamWriter::Error::InvalidCharacter);
QVERIFY(!writer.errorString().isEmpty());
QCOMPARE(buffer, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
"<root><text>element</text><!--A comment--><emptyElement/>"
"<![CDATA[CDATA]]>&entityReference;<?PI?>Characters"_ba);
}
{
// Encoding error: lone low surrogate
QByteArray buffer;
QXmlStreamWriter writer(&buffer);
writer.writeStartDocument();
writer.setStopWritingOnError(true);
writer.writeStartElement(u"root");
writer.writeCharacters(QChar(0xDC00)); // Stop writing from here
QVERIFY(writer.hasError());
QCOMPARE(writer.error(), QXmlStreamWriter::Error::EncodingError);
QVERIFY(!writer.errorString().isEmpty());
QCOMPARE(buffer, "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>"_ba);
writer.writeCharacters(u"I am a valid sentence");
writer.writeCharacters(u"But I won't be written until the setting is changed.");
writer.setStopWritingOnError(false);
writer.writeCharacters(u"Resume writing!");
writer.writeEndElement();
writer.writeEndDocument();
// Changing the flag doesn't clear the error; it just allows writing again.
QVERIFY(writer.hasError());
QCOMPARE(writer.error(), QXmlStreamWriter::Error::EncodingError);
QVERIFY(!writer.errorString().isEmpty());
QCOMPARE(buffer, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
"<root>Resume writing!</root>\n"_ba);
writer.setStopWritingOnError(true);
writer.writeCharacters(u"Valid characters rules!");
QVERIFY(writer.hasError());
QCOMPARE(writer.error(), QXmlStreamWriter::Error::EncodingError);
QVERIFY(!writer.errorString().isEmpty());
// Re-enabling stopWritingOnError does not clear the error state.
// Since the writer is still in error, further writes are ignored even if valid.
QCOMPARE(buffer, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
"<root>Resume writing!</root>\n"_ba);
}
{
QByteArray buffer;
QXmlStreamWriter writer(&buffer);
writer.setStopWritingOnError(true);
writer.writeStartDocument();
writer.writeStartElement(u"root");
writer.writeCharacters(u"Some characters");
writer.raiseError(u"Raising custom error"_s);
writer.writeCharacters(u"No more writing for you.");
writer.writeEndElement();
writer.writeEndDocument();
QVERIFY(writer.hasError());
QCOMPARE(writer.error(), QXmlStreamWriter::Error::CustomError);
QVERIFY(!writer.errorString().isEmpty());
QCOMPARE(buffer, "<?xml version=\"1.0\" encoding=\"UTF-8\"?><root>Some characters"_ba);
}
}
void tst_QXmlStream::invalidStringCharacters() const