diff --git a/src/corelib/serialization/qxmlstream.cpp b/src/corelib/serialization/qxmlstream.cpp index efbb460616f..1a161dc7b88 100644 --- a/src/corelib/serialization/qxmlstream.cpp +++ b/src/corelib/serialization/qxmlstream.cpp @@ -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. diff --git a/src/corelib/serialization/qxmlstream.h b/src/corelib/serialization/qxmlstream.h index cb9694b336a..becacd87cb1 100644 --- a/src/corelib/serialization/qxmlstream.h +++ b/src/corelib/serialization/qxmlstream.h @@ -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); diff --git a/tests/auto/corelib/serialization/qxmlstream/tst_qxmlstream.cpp b/tests/auto/corelib/serialization/qxmlstream/tst_qxmlstream.cpp index 7db97017458..81dc49eba54 100644 --- a/tests/auto/corelib/serialization/qxmlstream/tst_qxmlstream.cpp +++ b/tests/auto/corelib/serialization/qxmlstream/tst_qxmlstream.cpp @@ -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, "" + "Invalid characterelement" + "" + "Invalid characterelement" + "" + "Let's raise another error!" + "element"_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, "" + "Invalid characterelement" + "" + "Let's raise another error!element" + "I'm still standin' better than I ever did!\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 \"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, "" + "Valid & possible to <escape> "characters"" + "element" + "\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, ""_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, "" + "element" + "&entityReference;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, ""_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, "" + "Resume writing!\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, "" + "Resume writing!\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, "Some characters"_ba); + } } void tst_QXmlStream::invalidStringCharacters() const