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 <thiago.macieira@intel.com>
This commit is contained in:
Magdalena Stojek 2025-03-13 13:38:30 +01:00
parent d359035c80
commit c3295bd59c
2 changed files with 89 additions and 2 deletions

View File

@ -2913,6 +2913,8 @@ public:
uint hasIoError :1; uint hasIoError :1;
uint hasEncodingError :1; uint hasEncodingError :1;
uint autoFormatting :1; uint autoFormatting :1;
uint didWriteStartDocument :1;
uint didWriteAnyToken :1;
std::string autoFormattingIndent; std::string autoFormattingIndent;
NamespaceDeclaration emptyNamespace; NamespaceDeclaration emptyNamespace;
qsizetype lastNamespaceDeclaration; qsizetype lastNamespaceDeclaration;
@ -2946,6 +2948,8 @@ QXmlStreamWriterPrivate::QXmlStreamWriterPrivate(QXmlStreamWriter *q)
lastNamespaceDeclaration = 1; lastNamespaceDeclaration = 1;
autoFormatting = false; autoFormatting = false;
namespacePrefixCount = 0; namespacePrefixCount = 0;
didWriteStartDocument = false;
didWriteAnyToken = false;
} }
void QXmlStreamWriterPrivate::write(QAnyStringView s) void QXmlStreamWriterPrivate::write(QAnyStringView s)
@ -3065,6 +3069,7 @@ void QXmlStreamWriterPrivate::writeNamespaceDeclaration(const NamespaceDeclarati
write(namespaceDeclaration.namespaceUri); write(namespaceDeclaration.namespaceUri);
write("\""); write("\"");
} }
didWriteAnyToken = true;
} }
bool QXmlStreamWriterPrivate::finishStartElement(bool contents) bool QXmlStreamWriterPrivate::finishStartElement(bool contents)
@ -3084,6 +3089,7 @@ bool QXmlStreamWriterPrivate::finishStartElement(bool contents)
} }
inStartElement = inEmptyElement = false; inStartElement = inEmptyElement = false;
lastNamespaceDeclaration = namespaceDeclarations.size(); lastNamespaceDeclaration = namespaceDeclarations.size();
didWriteAnyToken = true;
return hadSomethingWritten; return hadSomethingWritten;
} }
@ -3149,7 +3155,8 @@ QXmlStreamPrivateTagStack::NamespaceDeclaration &QXmlStreamWriterPrivate::findNa
void QXmlStreamWriterPrivate::indent(int level) void QXmlStreamWriterPrivate::indent(int level)
{ {
write("\n"); if (didWriteStartDocument || didWriteAnyToken)
write("\n");
for (int i = 0; i < level; ++i) for (int i = 0; i < level; ++i)
write(autoFormattingIndent); write(autoFormattingIndent);
} }
@ -3375,6 +3382,7 @@ void QXmlStreamWriter::writeAttribute(QAnyStringView qualifiedName, QAnyStringVi
d->write("=\""); d->write("=\"");
d->writeEscaped(value, true); d->writeEscaped(value, true);
d->write("\""); d->write("\"");
d->didWriteAnyToken = true;
} }
/*! Writes an attribute with \a name and \a value, prefixed for /*! Writes an attribute with \a name and \a value, prefixed for
@ -3403,6 +3411,7 @@ void QXmlStreamWriter::writeAttribute(QAnyStringView namespaceUri, QAnyStringVie
d->write("=\""); d->write("=\"");
d->writeEscaped(value, true); d->writeEscaped(value, true);
d->write("\""); d->write("\"");
d->didWriteAnyToken = true;
} }
/*! /*!
@ -3610,7 +3619,8 @@ void QXmlStreamWriter::writeEndDocument()
Q_D(QXmlStreamWriter); Q_D(QXmlStreamWriter);
while (d->tagStack.size()) while (d->tagStack.size())
writeEndElement(); writeEndElement();
d->write("\n"); if (d->didWriteStartDocument || d->didWriteAnyToken)
d->write("\n");
} }
/*! /*!
@ -3621,6 +3631,7 @@ void QXmlStreamWriter::writeEndDocument()
void QXmlStreamWriter::writeEndElement() void QXmlStreamWriter::writeEndElement()
{ {
Q_D(QXmlStreamWriter); Q_D(QXmlStreamWriter);
Q_ASSERT(d->didWriteAnyToken);
if (d->tagStack.isEmpty()) if (d->tagStack.isEmpty())
return; return;
@ -3744,6 +3755,7 @@ void QXmlStreamWriter::writeProcessingInstruction(QAnyStringView target, QAnyStr
d->write(data); d->write(data);
} }
d->write("?>"); d->write("?>");
d->didWriteAnyToken = true;
} }
@ -3778,6 +3790,7 @@ void QXmlStreamWriter::writeStartDocument(QAnyStringView version)
if (d->device) // stringDevice does not get any encoding if (d->device) // stringDevice does not get any encoding
d->write("\" encoding=\"UTF-8"); d->write("\" encoding=\"UTF-8");
d->write("\"?>"); d->write("\"?>");
d->didWriteStartDocument = true;
} }
/*! Writes a document start with the XML version number \a version /*! 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\"?>"); d->write("\" standalone=\"yes\"?>");
else else
d->write("\" standalone=\"no\"?>"); d->write("\" standalone=\"no\"?>");
d->didWriteStartDocument = true;
} }
@ -3862,6 +3876,7 @@ void QXmlStreamWriterPrivate::writeStartElement(QAnyStringView namespaceUri, QAn
writeNamespaceDeclaration(namespaceDeclarations[i]); writeNamespaceDeclaration(namespaceDeclarations[i]);
} }
tag.namespaceDeclarationsSize = lastNamespaceDeclaration; tag.namespaceDeclarationsSize = lastNamespaceDeclaration;
didWriteAnyToken = true;
} }
/*! Writes the current state of the \a reader. All possible valid /*! Writes the current state of the \a reader. All possible valid

View File

@ -559,6 +559,10 @@ private slots:
void writerAutoFormattingWithProcessingInstructions() const; void writerAutoFormattingWithProcessingInstructions() const;
void writerAutoEmptyTags() const; void writerAutoEmptyTags() const;
void writeAttributesWithSpace() const; void writeAttributesWithSpace() const;
void writerAutoFormattingProcessingInstructionFirst() const;
void writerAutoFormattingStartElementFirst() const;
void writerAutoFormattingCommentFirst() const;
void writerAutoFormattingNamespaceFirst() const;
void addExtraNamespaceDeclarations(); void addExtraNamespaceDeclarations();
void setEntityResolver(); void setEntityResolver();
void readFromQBuffer() const; void readFromQBuffer() const;
@ -1075,6 +1079,74 @@ void tst_QXmlStream::writeAttributesWithSpace() const
QCOMPARE(buffer.buffer().data(), s.toUtf8().data()); 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 =
"<?B?>\n<!--This is a comment-->\n<A xmlns:website=\"http://website.com\" xmlns=\"http://websiteNo2.com\"/>\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 = "<A xmlns:website=\"http://website.com\">\n <!--This is a comment-->\n <?B?>\n</A>\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 = "<!--This is a comment--><A xmlns:website=\"http://website.com\"/>\n<?B?>\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 = "<A xmlns:website=\"http://website.com\" xmlns=\"http://websiteNo2.com\">\n <?B?></A>\n<!--This is a comment-->\n";
QCOMPARE(buffer.buffer().data(), str);
}
void tst_QXmlStream::writerAutoEmptyTags() const void tst_QXmlStream::writerAutoEmptyTags() const
{ {
QBuffer buffer; QBuffer buffer;