QXmlStreamWriter: add error-handling API

This change introduces QXmlStreamWriter::Error enum and three related
functions to enable error reporting during XML writing operations:
- error(): returns the current error state of the writer.
- errorString(): returns the corresponding error message.
- raiseError(): allows applications to raise custom write errors.

This complements the existing hasError() method and aligns the writer
with QXmlStreamReader's error handling.

[ChangeLog][QtCore][QXmlStreamWriter] Added error handling API with
QXmlStreamWriter::Error enum, error(), errorString(), and raiseError()
functions.

Fixes: QTBUG-82389
Change-Id: I4d57a9f611a303cf8dc05caf23b6d331a61684f9
Reviewed-by: Ivan Solovev <ivan.solovev@qt.io>
Reviewed-by: Edward Welbourne <edward.welbourne@qt.io>
This commit is contained in:
Magdalena Stojek 2025-04-04 17:07:01 +02:00
parent 9edcc46906
commit 53622aca2a
3 changed files with 237 additions and 27 deletions

View File

@ -3015,8 +3015,12 @@ QStringView QXmlStreamReader::documentEncoding() const
QXmlStreamWriter always encodes XML in UTF-8.
If an error occurs while writing to the underlying device, hasError()
starts returning true and subsequent writes are ignored.
\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.
The \l{QXmlStream Bookmarks Example} illustrates how to use a
stream writer to write an XML bookmark file (XBEL) that
@ -3024,6 +3028,29 @@ QStringView QXmlStreamReader::documentEncoding() const
*/
/*!
\enum QXmlStreamWriter::Error
This enum specifies the different error cases that can occur
when writing XML with QXmlStreamWriter.
\value NoError No error has occurred.
\value IOError An I/O error occurred while writing to the
device.
\value EncodingError An encoding error occurred while converting
characters to the output format.
\value InvalidCharacter A character not permitted in XML 1.0
was encountered while writing.
\value CustomError A custom error has been raised with
\l raiseError().
\since 6.10
*/
#if QT_CONFIG(xmlstreamwriter)
class QXmlStreamWriterPrivate : public QXmlStreamPrivateTagStack
@ -3042,6 +3069,8 @@ public:
delete device;
}
void raiseError(QXmlStreamWriter::Error error);
void raiseError(QXmlStreamWriter::Error error, const QString &message);
void write(QAnyStringView s);
void writeEscaped(QAnyStringView, bool escapeWhitespace = false);
bool finishStartElement(bool contents = true);
@ -3054,14 +3083,14 @@ public:
uint inEmptyElement :1;
uint lastWasStartElement :1;
uint wroteSomething :1;
uint hasIoError :1;
uint hasEncodingError :1;
uint autoFormatting :1;
uint didWriteStartDocument :1;
uint didWriteAnyToken :1;
std::string autoFormattingIndent = std::string(4, ' ');
NamespaceDeclaration emptyNamespace;
qsizetype lastNamespaceDeclaration = 1;
QXmlStreamWriter::Error error = QXmlStreamWriter::Error::NoError;
QString errorString;
NamespaceDeclaration &addExtraNamespace(QAnyStringView namespaceUri, QAnyStringView prefix);
NamespaceDeclaration &findNamespace(QAnyStringView namespaceUri, bool writeDeclaration = false, bool noDefault = false);
@ -3080,16 +3109,42 @@ private:
QXmlStreamWriterPrivate::QXmlStreamWriterPrivate(QXmlStreamWriter *q)
: q_ptr(q), deleteDevice(false), inStartElement(false),
inEmptyElement(false), lastWasStartElement(false),
wroteSomething(false), hasIoError(false),
hasEncodingError(false), autoFormatting(false),
wroteSomething(false), autoFormatting(false),
didWriteStartDocument(false), didWriteAnyToken(false)
{
}
void QXmlStreamWriterPrivate::raiseError(QXmlStreamWriter::Error errorCode)
{
error = errorCode;
switch (error) {
case QXmlStreamWriter::Error::IOError:
errorString = QXmlStream::tr("An I/O error occurred while writing");
break;
case QXmlStreamWriter::Error::EncodingError:
errorString = QXmlStream::tr("An encoding error occurred while writing");
break;
case QXmlStreamWriter::Error::InvalidCharacter:
errorString = QXmlStream::tr("Encountered an invalid XML 1.0 character while writing");
break;
case QXmlStreamWriter::Error::CustomError:
errorString = QXmlStream::tr("An error occurred while writing");
break;
case QXmlStreamWriter::Error::NoError:
break;
}
}
void QXmlStreamWriterPrivate::raiseError(QXmlStreamWriter::Error errorCode, const QString &message)
{
error = errorCode;
errorString = message;
}
void QXmlStreamWriterPrivate::write(QAnyStringView s)
{
if (device) {
if (hasIoError)
if (error == QXmlStreamWriter::Error::IOError)
return;
s.visit([&] (auto s) { doWriteToDevice(s); });
@ -3102,27 +3157,36 @@ void QXmlStreamWriterPrivate::write(QAnyStringView s)
void QXmlStreamWriterPrivate::writeEscaped(QAnyStringView s, bool escapeWhitespace)
{
struct NextResult {
char32_t value;
bool encodingError;
};
struct NextLatin1 {
char32_t operator()(const char *&it, const char *) const
{ return uchar(*it++); }
NextResult operator()(const char *&it, const char *) const
{ return {uchar(*it++), false}; }
};
struct NextUtf8 {
char32_t operator()(const char *&it, const char *end) const
NextResult operator()(const char *&it, const char *end) const
{
uchar uc = *it++;
char32_t utf32 = 0;
char32_t *output = &utf32;
qsizetype n = QUtf8Functions::fromUtf8<QUtf8BaseTraits>(uc, output, it, end);
return n < 0 ? 0 : utf32;
return n < 0 ? NextResult{0, true} : NextResult{utf32, false};
}
};
struct NextUtf16 {
char32_t operator()(const QChar *&it, const QChar *end) const
NextResult operator()(const QChar *&it, const QChar *end) const
{
QStringIterator decoder(it, end);
char32_t result = decoder.next(u'\0');
// We can have '\0' in the text, and it should be reported as
// InvalidCharacter, not as EncodingError
constexpr char32_t invalidValue = 0xFFFFFFFF;
Q_ASSERT(invalidValue > QChar::LastValidCodePoint);
char32_t result = decoder.next(invalidValue);
it = decoder.position();
return result;
return result == invalidValue ? NextResult{U'\0', true}
: NextResult{result, false};
}
};
@ -3143,7 +3207,7 @@ void QXmlStreamWriterPrivate::writeEscaped(QAnyStringView s, bool escapeWhitespa
while (it != end) {
auto next_it = it;
char32_t uc = decoder(next_it, end);
auto [uc, encodingError] = decoder(next_it, end);
if (uc == u'<') {
replacement = "&lt;"_L1;
break;
@ -3167,7 +3231,7 @@ void QXmlStreamWriterPrivate::writeEscaped(QAnyStringView s, bool escapeWhitespa
break;
}
} else if (uc == u'\v' || uc == u'\f') {
hasEncodingError = true;
raiseError(QXmlStreamWriter::Error::InvalidCharacter);
break;
} else if (uc == u'\r') {
if (escapeWhitespace) {
@ -3175,7 +3239,10 @@ void QXmlStreamWriterPrivate::writeEscaped(QAnyStringView s, bool escapeWhitespa
break;
}
} else if (uc <= u'\x1F' || uc == u'\uFFFE' || uc == u'\uFFFF') {
hasEncodingError = true;
if (encodingError)
raiseError(QXmlStreamWriter::Error::EncodingError);
else
raiseError(QXmlStreamWriter::Error::InvalidCharacter);
break;
}
it = next_it;
@ -3307,14 +3374,14 @@ void QXmlStreamWriterPrivate::doWriteToDevice(QStringView s)
s = s.sliced(chunkSize);
}
if (state.remainingChars > 0)
hasEncodingError = true;
raiseError(QXmlStreamWriter::Error::EncodingError);
}
void QXmlStreamWriterPrivate::doWriteToDevice(QUtf8StringView s)
{
QByteArrayView bytes = s;
if (device->write(bytes.data(), bytes.size()) != bytes.size())
hasIoError = true;
raiseError(QXmlStreamWriter::Error::IOError);
}
void QXmlStreamWriterPrivate::doWriteToDevice(QLatin1StringView s)
@ -3481,18 +3548,66 @@ int QXmlStreamWriter::autoFormattingIndent() const
}
/*!
Returns \c true if writing failed.
Returns \c true if an error occurred while trying to write data.
This can happen if the stream failed to write to the underlying
device or if the data to be written contained invalid characters.
If the error is \l Error::IOError, subsequent writes to the underlying
QIODevice will fail. In other cases malformed data might be written to
the document.
The error status is never reset. Writes happening after the error
occurred may be ignored, even if the error condition is cleared.
\sa error(), errorString(), raiseError(const QString &message),
*/
bool QXmlStreamWriter::hasError() const
{
return error() != QXmlStreamWriter::Error::NoError;
}
/*!
Returns the current error state of the writer.
If no error has occurred, this function returns
QXmlStreamWriter::Error::NoError.
\since 6.10
\sa errorString(), raiseError(const QString &message), hasError()
*/
QXmlStreamWriter::Error QXmlStreamWriter::error() const
{
Q_D(const QXmlStreamWriter);
return d->hasIoError || d->hasEncodingError;
return d->error;
}
/*!
If an error has occurred, returns its associated error message.
The error message is either set internally by QXmlStreamWriter or provided
by the user via raiseError(). If no error has occured, this function returns
a null string.
\since 6.10
\sa error(), raiseError(const QString &message), hasError()
*/
QString QXmlStreamWriter::errorString() const
{
Q_D(const QXmlStreamWriter);
return d->errorString;
}
/*!
Raises a custom error with the given \a message.
This function is for manual indication that an error has occurred during
writing, such as an application level validation failure.
\since 6.10
\sa errorString(), error(), hasError()
*/
void QXmlStreamWriter::raiseError(const QString &message)
{
Q_D(QXmlStreamWriter);
d->raiseError(QXmlStreamWriter::Error::CustomError, message);
}
/*!

View File

@ -459,6 +459,17 @@ public:
void writeCurrentToken(const QXmlStreamReader &reader);
#endif
enum class Error {
NoError,
IOError,
EncodingError,
InvalidCharacter,
CustomError,
};
void raiseError(const QString &message);
QString errorString() const;
Error error() const;
bool hasError() const;
private:

View File

@ -605,7 +605,7 @@ private slots:
void crashInXmlStreamReader() const;
void invalidStringCharacters_data() const;
void invalidStringCharacters() const;
void hasError() const;
void writerErrors() const;
void readBack_data() const;
void readBack() const;
void roundTrip() const;
@ -2048,6 +2048,7 @@ void tst_QXmlStream::writeBadCharactersUtf8() const
QXmlStreamWriter writer(&target);
writer.writeTextElement("a", QUtf8StringView(input));
QVERIFY(writer.hasError());
QCOMPARE(writer.error(), QXmlStreamWriter::Error::EncodingError);
}
void tst_QXmlStream::writeBadCharactersUtf16_data() const
@ -2066,6 +2067,8 @@ void tst_QXmlStream::writeBadCharactersUtf16() const
QXmlStreamWriter writer(&target);
writer.writeTextElement("a", input);
QVERIFY(writer.hasError());
QCOMPARE(writer.error(), QXmlStreamWriter::Error::EncodingError);
}
void tst_QXmlStream::entitiesAndWhitespace_1() const
@ -2257,10 +2260,10 @@ protected:
public:
void setCapacity(int capacity) { m_capacity = capacity; }
private:
qint64 m_capacity;
qint64 m_capacity = 0;
};
void tst_QXmlStream::hasError() const
void tst_QXmlStream::writerErrors() const
{
{
FakeBuffer fb;
@ -2270,6 +2273,8 @@ void tst_QXmlStream::hasError() const
writer.writeStartDocument();
writer.writeEndDocument();
QVERIFY(!writer.hasError());
QCOMPARE(writer.error(), QXmlStreamWriter::Error::NoError);
QVERIFY(writer.errorString().isEmpty());
QCOMPARE(fb.data(), QByteArray("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"));
}
@ -2282,6 +2287,8 @@ void tst_QXmlStream::hasError() const
QXmlStreamWriter writer(&fb);
writer.writeStartDocument();
QVERIFY(writer.hasError());
QCOMPARE(writer.error(), QXmlStreamWriter::Error::IOError);
QVERIFY(!writer.errorString().isEmpty());
QCOMPARE(fb.data(), expected);
}
@ -2294,6 +2301,8 @@ void tst_QXmlStream::hasError() const
QXmlStreamWriter writer(&fb);
writer.writeStartDocument();
QVERIFY(writer.hasError());
QCOMPARE(writer.error(), QXmlStreamWriter::Error::IOError);
QVERIFY(!writer.errorString().isEmpty());
QCOMPARE(fb.data(), expected);
}
@ -2301,13 +2310,16 @@ void tst_QXmlStream::hasError() const
// Failure caused by write(QStringRef)
FakeBuffer fb;
QVERIFY(fb.open(QBuffer::ReadWrite));
const QByteArray expected = QByteArrayLiteral("<?xml version=\"1.0\" encoding=\"UTF-8\"?><test xmlns:");
const QByteArray expected =
QByteArrayLiteral("<?xml version=\"1.0\" encoding=\"UTF-8\"?><test xmlns:");
fb.setCapacity(expected.size());
QXmlStreamWriter writer(&fb);
writer.writeStartDocument();
writer.writeStartElement("test");
writer.writeNamespace("http://foo.bar", "foo");
QVERIFY(writer.hasError());
QCOMPARE(writer.error(), QXmlStreamWriter::Error::IOError);
QVERIFY(!writer.errorString().isEmpty());
QCOMPARE(fb.data(), expected);
}
@ -2319,14 +2331,86 @@ void tst_QXmlStream::hasError() const
QXmlStreamWriter writer(&fb);
writer.writeStartDocument();
QVERIFY(writer.hasError());
QCOMPARE(writer.error(), QXmlStreamWriter::Error::IOError);
QCOMPARE(fb.data(), QByteArray("<?xml vers"));
fb.setCapacity(1000);
writer.writeStartElement("test"); // literal & qstring
writer.writeNamespace("http://foo.bar", "foo"); // literal & qstringref
QVERIFY(writer.hasError());
QCOMPARE(writer.error(), QXmlStreamWriter::Error::IOError);
QVERIFY(!writer.errorString().isEmpty());
QCOMPARE(fb.data(), QByteArray("<?xml vers"));
}
{
// Encoding error: lone high surrogate
QByteArray buffer;
QXmlStreamWriter writer(&buffer);
writer.writeStartDocument();
writer.writeStartElement("root");
writer.writeCharacters(QChar(0xD800));
writer.writeEndElement();
writer.writeEndDocument();
QVERIFY(writer.hasError());
QCOMPARE(writer.error(), QXmlStreamWriter::Error::EncodingError);
QVERIFY(!writer.errorString().isEmpty());
}
{
// Invalid character error: invalid character for XML 1.0 in text content
QByteArray buffer;
QXmlStreamWriter writer(&buffer);
writer.writeStartDocument();
writer.writeStartElement("root"_L1);
writer.writeCharacters("Invalid \v character"_L1); // \v is invalid in XML 1.0
writer.writeEndElement();
writer.writeEndDocument();
QVERIFY(writer.hasError());
QCOMPARE(writer.error(), QXmlStreamWriter::Error::InvalidCharacter);
QVERIFY(!writer.errorString().isEmpty());
}
{
// Invalid character error: forbidden control character for XML 1.0 U+0001
QByteArray buffer;
QXmlStreamWriter writer(&buffer);
writer.writeStartDocument();
writer.writeStartElement("root"_L1);
writer.writeCharacters("Invalid \x01 character"_L1);
writer.writeEndElement();
writer.writeEndDocument();
QVERIFY(writer.hasError());
QCOMPARE(writer.error(), QXmlStreamWriter::Error::InvalidCharacter);
QVERIFY(!writer.errorString().isEmpty());
}
{
// '\0' is an InvalidCharacter, not an EncodingError
QByteArray buffer;
QXmlStreamWriter writer(&buffer);
writer.writeStartDocument();
writer.writeStartElement("root"_L1);
writer.writeCharacters("Invalid \0 character"_L1);
writer.writeEndElement();
writer.writeEndDocument();
QVERIFY(writer.hasError());
QCOMPARE(writer.error(), QXmlStreamWriter::Error::InvalidCharacter);
QVERIFY(!writer.errorString().isEmpty());
}
{
// Custom error raised by user
QByteArray buffer;
QXmlStreamWriter writer(&buffer);
writer.writeStartDocument();
writer.writeStartElement("root"_L1);
writer.raiseError("Custom error");
writer.writeEndDocument();
QVERIFY(writer.hasError());
QCOMPARE(writer.error(), QXmlStreamWriter::Error::CustomError);
QCOMPARE(writer.errorString(), "Custom error"_L1);
}
}
void tst_QXmlStream::invalidStringCharacters() const