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. QXmlStreamWriter always encodes XML in UTF-8.
\note If an error occurs while writing, \l hasError() will return true. If an error occurs while writing, \l hasError() will return true.
However, data that was already buffered at the time the error occurred, However, by default, data that was already buffered at the time the error
or data written from within the same operation, may still be written occurred, or data written from within the same operation, may still be
to the underlying device. This applies to both \l EncodingError and written to the underlying device. This applies to \l Error::EncodingError,
user-raised \l CustomError. Applications should treat the error state \l Error::InvalidCharacter, and user-raised \l Error::CustomError.
as terminal and avoid further use of the writer after an error. 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 The \l{QXmlStream Bookmarks Example} illustrates how to use a
stream writer to write an XML bookmark file (XBEL) that stream writer to write an XML bookmark file (XBEL) that
@ -3146,6 +3151,7 @@ public:
uint autoFormatting :1; uint autoFormatting :1;
uint didWriteStartDocument :1; uint didWriteStartDocument :1;
uint didWriteAnyToken :1; uint didWriteAnyToken :1;
uint stopWritingOnError :1;
std::string autoFormattingIndent = std::string(4, ' '); std::string autoFormattingIndent = std::string(4, ' ');
NamespaceDeclaration emptyNamespace; NamespaceDeclaration emptyNamespace;
qsizetype lastNamespaceDeclaration = 1; qsizetype lastNamespaceDeclaration = 1;
@ -3170,7 +3176,8 @@ QXmlStreamWriterPrivate::QXmlStreamWriterPrivate(QXmlStreamWriter *q)
: q_ptr(q), deleteDevice(false), inStartElement(false), : q_ptr(q), deleteDevice(false), inStartElement(false),
inEmptyElement(false), lastWasStartElement(false), inEmptyElement(false), lastWasStartElement(false),
wroteSomething(false), autoFormatting(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) void QXmlStreamWriterPrivate::write(QAnyStringView s)
{ {
if (stopWritingOnError && (error != QXmlStreamWriter::Error::NoError))
return;
if (device) { if (device) {
if (error == QXmlStreamWriter::Error::IOError) if (error == QXmlStreamWriter::Error::IOError)
return; return;
@ -3296,6 +3305,8 @@ void QXmlStreamWriterPrivate::writeEscaped(QAnyStringView s, bool escapeWhitespa
case u'\v': case u'\v':
case u'\f': case u'\f':
raiseError(QXmlStreamWriter::Error::InvalidCharacter); raiseError(QXmlStreamWriter::Error::InvalidCharacter);
if (stopWritingOnError)
return;
replacement = ""_L1; replacement = ""_L1;
Q_ASSERT(!replacement.isNull()); Q_ASSERT(!replacement.isNull());
break; break;
@ -3309,6 +3320,8 @@ void QXmlStreamWriterPrivate::writeEscaped(QAnyStringView s, bool escapeWhitespa
raiseError(encodingError raiseError(encodingError
? QXmlStreamWriter::Error::EncodingError ? QXmlStreamWriter::Error::EncodingError
: QXmlStreamWriter::Error::InvalidCharacter); : QXmlStreamWriter::Error::InvalidCharacter);
if (stopWritingOnError)
return;
replacement = ""_L1; replacement = ""_L1;
Q_ASSERT(!replacement.isNull()); Q_ASSERT(!replacement.isNull());
break; break;
@ -3617,6 +3630,35 @@ int QXmlStreamWriter::autoFormattingIndent() const
return indent.count(u' ') - indent.count(u'\t'); 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. 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(bool autoFormatting READ autoFormatting WRITE setAutoFormatting)
QDOC_PROPERTY(int autoFormattingIndent READ autoFormattingIndent WRITE setAutoFormattingIndent) QDOC_PROPERTY(int autoFormattingIndent READ autoFormattingIndent WRITE setAutoFormattingIndent)
QDOC_PROPERTY(bool stopWritingOnError READ stopWritingOnError WRITE setStopWritingOnError)
public: public:
QXmlStreamWriter(); QXmlStreamWriter();
explicit QXmlStreamWriter(QIODevice *device); explicit QXmlStreamWriter(QIODevice *device);
@ -393,6 +394,9 @@ public:
void setAutoFormattingIndent(int spacesOrTabs); void setAutoFormattingIndent(int spacesOrTabs);
int autoFormattingIndent() const; int autoFormattingIndent() const;
void setStopWritingOnError(bool stop);
bool stopWritingOnError() const;
#if QT_CORE_REMOVED_SINCE(6,5) #if QT_CORE_REMOVED_SINCE(6,5)
void writeAttribute(const QString &qualifiedName, const QString &value); void writeAttribute(const QString &qualifiedName, const QString &value);
void writeAttribute(const QString &namespaceUri, const QString &name, 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_data() const;
void invalidStringCharacters() const; void invalidStringCharacters() const;
void writerErrors() const; void writerErrors() const;
void stopWritingOnError() const;
void readBack_data() const; void readBack_data() const;
void readBack() const; void readBack() const;
void roundTrip() const; void roundTrip() const;
@ -2404,7 +2405,201 @@ void tst_QXmlStream::writerErrors() const
QCOMPARE(writer.error(), QXmlStreamWriter::Error::CustomError); QCOMPARE(writer.error(), QXmlStreamWriter::Error::CustomError);
QCOMPARE(writer.errorString(), "Custom error"_L1); 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 void tst_QXmlStream::invalidStringCharacters() const