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:
parent
9edcc46906
commit
53622aca2a
@ -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 = "<"_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);
|
||||
}
|
||||
|
||||
/*!
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user