From c3295bd59cb1c3b858b6a858aba4481adeea209b Mon Sep 17 00:00:00 2001 From: Magdalena Stojek Date: Thu, 13 Mar 2025 13:38:30 +0100 Subject: [PATCH] QXmlStreamWriter: Ensure correct newline handling with auto-formatting Previously, QXmlStreamWriter would incorrectly insert a newline before the first token when writeStartDocument() was not used, while auto-formatting was enabled. This fix ensures that the first token is written inline without an extra leading newline, while preserving expected formatting for subsequent tokens. To achieve this, two new flags have been introduced: - didWriteStartDocument: Tracks whether writeStartDocument() was called. - didWriteAnyToken: Ensures that at least one token has been written before allowing newlines. Fixes: QTBUG-28721 Pick-to: 6.9 6.8 Change-Id: I8be7e8fc6ac0e63304359d24c6c8372e5ba42bb4 Reviewed-by: Thiago Macieira --- src/corelib/serialization/qxmlstream.cpp | 19 ++++- .../qxmlstream/tst_qxmlstream.cpp | 72 +++++++++++++++++++ 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/src/corelib/serialization/qxmlstream.cpp b/src/corelib/serialization/qxmlstream.cpp index 9dcdf490412..50f87900161 100644 --- a/src/corelib/serialization/qxmlstream.cpp +++ b/src/corelib/serialization/qxmlstream.cpp @@ -2913,6 +2913,8 @@ public: uint hasIoError :1; uint hasEncodingError :1; uint autoFormatting :1; + uint didWriteStartDocument :1; + uint didWriteAnyToken :1; std::string autoFormattingIndent; NamespaceDeclaration emptyNamespace; qsizetype lastNamespaceDeclaration; @@ -2946,6 +2948,8 @@ QXmlStreamWriterPrivate::QXmlStreamWriterPrivate(QXmlStreamWriter *q) lastNamespaceDeclaration = 1; autoFormatting = false; namespacePrefixCount = 0; + didWriteStartDocument = false; + didWriteAnyToken = false; } void QXmlStreamWriterPrivate::write(QAnyStringView s) @@ -3065,6 +3069,7 @@ void QXmlStreamWriterPrivate::writeNamespaceDeclaration(const NamespaceDeclarati write(namespaceDeclaration.namespaceUri); write("\""); } + didWriteAnyToken = true; } bool QXmlStreamWriterPrivate::finishStartElement(bool contents) @@ -3084,6 +3089,7 @@ bool QXmlStreamWriterPrivate::finishStartElement(bool contents) } inStartElement = inEmptyElement = false; lastNamespaceDeclaration = namespaceDeclarations.size(); + didWriteAnyToken = true; return hadSomethingWritten; } @@ -3149,7 +3155,8 @@ QXmlStreamPrivateTagStack::NamespaceDeclaration &QXmlStreamWriterPrivate::findNa void QXmlStreamWriterPrivate::indent(int level) { - write("\n"); + if (didWriteStartDocument || didWriteAnyToken) + write("\n"); for (int i = 0; i < level; ++i) write(autoFormattingIndent); } @@ -3375,6 +3382,7 @@ void QXmlStreamWriter::writeAttribute(QAnyStringView qualifiedName, QAnyStringVi d->write("=\""); d->writeEscaped(value, true); d->write("\""); + d->didWriteAnyToken = true; } /*! Writes an attribute with \a name and \a value, prefixed for @@ -3403,6 +3411,7 @@ void QXmlStreamWriter::writeAttribute(QAnyStringView namespaceUri, QAnyStringVie d->write("=\""); d->writeEscaped(value, true); d->write("\""); + d->didWriteAnyToken = true; } /*! @@ -3610,7 +3619,8 @@ void QXmlStreamWriter::writeEndDocument() Q_D(QXmlStreamWriter); while (d->tagStack.size()) writeEndElement(); - d->write("\n"); + if (d->didWriteStartDocument || d->didWriteAnyToken) + d->write("\n"); } /*! @@ -3621,6 +3631,7 @@ void QXmlStreamWriter::writeEndDocument() void QXmlStreamWriter::writeEndElement() { Q_D(QXmlStreamWriter); + Q_ASSERT(d->didWriteAnyToken); if (d->tagStack.isEmpty()) return; @@ -3744,6 +3755,7 @@ void QXmlStreamWriter::writeProcessingInstruction(QAnyStringView target, QAnyStr d->write(data); } d->write("?>"); + d->didWriteAnyToken = true; } @@ -3778,6 +3790,7 @@ void QXmlStreamWriter::writeStartDocument(QAnyStringView version) if (d->device) // stringDevice does not get any encoding d->write("\" encoding=\"UTF-8"); d->write("\"?>"); + d->didWriteStartDocument = true; } /*! Writes a document start with the XML version number \a version @@ -3801,6 +3814,7 @@ void QXmlStreamWriter::writeStartDocument(QAnyStringView version, bool standalon d->write("\" standalone=\"yes\"?>"); else d->write("\" standalone=\"no\"?>"); + d->didWriteStartDocument = true; } @@ -3862,6 +3876,7 @@ void QXmlStreamWriterPrivate::writeStartElement(QAnyStringView namespaceUri, QAn writeNamespaceDeclaration(namespaceDeclarations[i]); } tag.namespaceDeclarationsSize = lastNamespaceDeclaration; + didWriteAnyToken = true; } /*! Writes the current state of the \a reader. All possible valid diff --git a/tests/auto/corelib/serialization/qxmlstream/tst_qxmlstream.cpp b/tests/auto/corelib/serialization/qxmlstream/tst_qxmlstream.cpp index b90d05b0fa1..d40cb8fa8d0 100644 --- a/tests/auto/corelib/serialization/qxmlstream/tst_qxmlstream.cpp +++ b/tests/auto/corelib/serialization/qxmlstream/tst_qxmlstream.cpp @@ -559,6 +559,10 @@ private slots: void writerAutoFormattingWithProcessingInstructions() const; void writerAutoEmptyTags() const; void writeAttributesWithSpace() const; + void writerAutoFormattingProcessingInstructionFirst() const; + void writerAutoFormattingStartElementFirst() const; + void writerAutoFormattingCommentFirst() const; + void writerAutoFormattingNamespaceFirst() const; void addExtraNamespaceDeclarations(); void setEntityResolver(); void readFromQBuffer() const; @@ -1075,6 +1079,74 @@ void tst_QXmlStream::writeAttributesWithSpace() const QCOMPARE(buffer.buffer().data(), s.toUtf8().data()); } +void tst_QXmlStream::writerAutoFormattingProcessingInstructionFirst() const +{ + QBuffer buffer; + buffer.open(QIODevice::WriteOnly); + QXmlStreamWriter writer(&buffer); + writer.setAutoFormatting(true); + writer.writeProcessingInstruction("B"); + writer.writeComment("This is a comment"); + writer.writeStartElement("A"); + writer.writeNamespace("http://website.com", "website"); + writer.writeDefaultNamespace("http://websiteNo2.com"); + writer.writeEndElement(); + writer.writeEndDocument(); + const char *str = + "\n\n\n"; + QCOMPARE(buffer.buffer().data(), str); +} +void tst_QXmlStream::writerAutoFormattingStartElementFirst() const +{ + QBuffer buffer; + buffer.open(QIODevice::WriteOnly); + QXmlStreamWriter writer(&buffer); + writer.setAutoFormatting(true); + writer.writeStartElement("A"); + writer.writeNamespace("http://website.com", "website"); + writer.writeComment("This is a comment"); + writer.writeProcessingInstruction("B"); + writer.writeDefaultNamespace("http://websiteNo2.com"); + writer.writeEndElement(); + writer.writeEndDocument(); + const char *str = "\n \n \n\n"; + QCOMPARE(buffer.buffer().data(), str); +} + +void tst_QXmlStream::writerAutoFormattingCommentFirst() const +{ + QBuffer buffer; + buffer.open(QIODevice::WriteOnly); + QXmlStreamWriter writer(&buffer); + writer.setAutoFormatting(true); + writer.writeComment("This is a comment"); + writer.writeStartElement("A"); + writer.writeNamespace("http://website.com", "website"); + writer.writeEndElement(); + writer.writeDefaultNamespace("http://websiteNo2.com"); + writer.writeProcessingInstruction("B"); + writer.writeEndDocument(); + const char *str = "\n\n"; + QCOMPARE(buffer.buffer().data(), str); +} + +void tst_QXmlStream::writerAutoFormattingNamespaceFirst() const +{ + QBuffer buffer; + buffer.open(QIODevice::WriteOnly); + QXmlStreamWriter writer(&buffer); + writer.setAutoFormatting(true); + writer.writeNamespace("http://website.com", "website"); + writer.writeStartElement("A"); + writer.writeDefaultNamespace("http://websiteNo2.com"); + writer.writeProcessingInstruction("B"); + writer.writeEndElement(); + writer.writeComment("This is a comment"); + writer.writeEndDocument(); + const char *str = "\n \n\n"; + QCOMPARE(buffer.buffer().data(), str); +} + void tst_QXmlStream::writerAutoEmptyTags() const { QBuffer buffer;