blob: 42cb10100add70a14e79fc0b0dfdb9606e771434 [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2018 Intel Corporation.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the examples of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:BSD$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** BSD License Usage
** Alternatively, you may use this file under the terms of the BSD license
** as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
** * Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** * Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in
** the documentation and/or other materials provided with the
** distribution.
** * Neither the name of The Qt Company Ltd nor the names of its
** contributors may be used to endorse or promote products derived
** from this software without specific prior written permission.
**
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "xmlconverter.h"
#include <QBitArray>
#include <QtCborCommon>
#include <QFile>
#include <QFloat16>
#include <QMetaType>
#include <QRegularExpression>
#include <QUrl>
#include <QXmlStreamReader>
#include <QXmlStreamWriter>
static const char optionHelp[] =
"compact=no|yes Use compact XML form.\n";
static XmlConverter xmlConverter;
static QVariant variantFromXml(QXmlStreamReader &xml, Converter::Options options);
static QVariantList listFromXml(QXmlStreamReader &xml, Converter::Options options)
{
QVariantList list;
while (!xml.atEnd() && !(xml.isEndElement() && xml.name() == QLatin1String("list"))) {
xml.readNext();
switch (xml.tokenType()) {
case QXmlStreamReader::StartElement:
list << variantFromXml(xml, options);
continue;
case QXmlStreamReader::EndElement:
continue;
case QXmlStreamReader::Comment:
// ignore comments
continue;
case QXmlStreamReader::Characters:
// ignore whitespace
if (xml.isWhitespace())
continue;
Q_FALLTHROUGH();
default:
break;
}
fprintf(stderr, "%lld:%lld: Invalid XML %s '%s'.\n",
xml.lineNumber(), xml.columnNumber(),
qPrintable(xml.tokenString()), qPrintable(xml.name().toString()));
exit(EXIT_FAILURE);
}
xml.readNext();
return list;
}
static VariantOrderedMap::value_type mapEntryFromXml(QXmlStreamReader &xml, Converter::Options options)
{
QVariant key, value;
while (!xml.atEnd() && !(xml.isEndElement() && xml.name() == QLatin1String("entry"))) {
xml.readNext();
switch (xml.tokenType()) {
case QXmlStreamReader::StartElement:
if (value.isValid())
break;
if (key.isValid())
value = variantFromXml(xml, options);
else
key = variantFromXml(xml, options);
continue;
case QXmlStreamReader::EndElement:
continue;
case QXmlStreamReader::Comment:
// ignore comments
continue;
case QXmlStreamReader::Characters:
// ignore whitespace
if (xml.isWhitespace())
continue;
Q_FALLTHROUGH();
default:
break;
}
fprintf(stderr, "%lld:%lld: Invalid XML %s '%s'.\n",
xml.lineNumber(), xml.columnNumber(),
qPrintable(xml.tokenString()), qPrintable(xml.name().toString()));
exit(EXIT_FAILURE);
}
return { key, value };
}
static QVariant mapFromXml(QXmlStreamReader &xml, Converter::Options options)
{
QVariantMap map1;
VariantOrderedMap map2;
while (!xml.atEnd() && !(xml.isEndElement() && xml.name() == QLatin1String("map"))) {
xml.readNext();
switch (xml.tokenType()) {
case QXmlStreamReader::StartElement:
if (xml.name() == QLatin1String("entry")) {
auto pair = mapEntryFromXml(xml, options);
if (options & Converter::SupportsArbitraryMapKeys)
map2.append(pair);
else
map1.insert(pair.first.toString(), pair.second);
continue;
}
break;
case QXmlStreamReader::EndElement:
continue;
case QXmlStreamReader::Comment:
// ignore comments
continue;
case QXmlStreamReader::Characters:
// ignore whitespace
if (xml.isWhitespace())
continue;
Q_FALLTHROUGH();
default:
break;
}
fprintf(stderr, "%lld:%lld: Invalid XML %s '%s'.\n",
xml.lineNumber(), xml.columnNumber(),
qPrintable(xml.tokenString()), qPrintable(xml.name().toString()));
exit(EXIT_FAILURE);
}
xml.readNext();
if (options & Converter::SupportsArbitraryMapKeys)
return QVariant::fromValue(map2);
return map1;
}
static QVariant variantFromXml(QXmlStreamReader &xml, Converter::Options options)
{
QStringRef name = xml.name();
if (name == QLatin1String("list"))
return listFromXml(xml, options);
if (name == QLatin1String("map"))
return mapFromXml(xml, options);
if (name != QLatin1String("value")) {
fprintf(stderr, "%lld:%lld: Invalid XML key '%s'.\n",
xml.lineNumber(), xml.columnNumber(), qPrintable(name.toString()));
exit(EXIT_FAILURE);
}
QXmlStreamAttributes attrs = xml.attributes();
QStringRef type = attrs.value(QLatin1String("type"));
forever {
xml.readNext();
if (xml.isComment())
continue;
if (xml.isCDATA() || xml.isCharacters() || xml.isEndElement())
break;
fprintf(stderr, "%lld:%lld: Invalid XML %s '%s'.\n",
xml.lineNumber(), xml.columnNumber(),
qPrintable(xml.tokenString()), qPrintable(name.toString()));
exit(EXIT_FAILURE);
}
QStringRef text = xml.text();
if (!xml.isCDATA())
text = text.trimmed();
QVariant result;
bool ok;
if (type.isEmpty()) {
// ok
} else if (type == QLatin1String("number")) {
// try integer first
qint64 v = text.toLongLong(&ok);
if (ok) {
result = v;
} else {
// let's see floating point
double d = text.toDouble(&ok);
result = d;
if (!ok) {
fprintf(stderr, "%lld:%lld: Invalid XML: could not interpret '%s' as a number.\n",
xml.lineNumber(), xml.columnNumber(), qPrintable(text.toString()));
exit(EXIT_FAILURE);
}
}
} else if (type == QLatin1String("bytes")) {
QByteArray data = text.toLatin1();
QStringRef encoding = attrs.value("encoding");
if (encoding == QLatin1String("base64url")) {
result = QByteArray::fromBase64(data, QByteArray::Base64UrlEncoding);
} else if (encoding == QLatin1String("hex")) {
result = QByteArray::fromHex(data);
} else if (encoding.isEmpty() || encoding == QLatin1String("base64")) {
result = QByteArray::fromBase64(data);
} else {
fprintf(stderr, "%lld:%lld: Invalid XML: unknown encoding '%s' for bytes.\n",
xml.lineNumber(), xml.columnNumber(), qPrintable(encoding.toString()));
exit(EXIT_FAILURE);
}
} else if (type == QLatin1String("string")) {
result = text.toString();
} else if (type == QLatin1String("null")) {
result = QVariant::fromValue(nullptr);
} else if (type == QLatin1String("CBOR simple type")) {
result = QVariant::fromValue(QCborSimpleType(text.toShort()));
} else if (type == QLatin1String("bits")) {
QBitArray ba;
ba.resize(text.size());
qsizetype n = 0;
for (qsizetype i = 0; i < text.size(); ++i) {
QChar c = text.at(i);
if (c == '1') {
ba.setBit(n++);
} else if (c == '0') {
++n;
} else if (!c.isSpace()) {
fprintf(stderr, "%lld:%lld: Invalid XML: invalid bit string '%s'.\n",
xml.lineNumber(), xml.columnNumber(), qPrintable(text.toString()));
exit(EXIT_FAILURE);
}
}
ba.resize(n);
result = ba;
} else {
int id = QMetaType::UnknownType;
if (type == QLatin1String("datetime"))
id = QMetaType::QDateTime;
else if (type == QLatin1String("url"))
id = QMetaType::QUrl;
else if (type == QLatin1String("uuid"))
id = QMetaType::QUuid;
else if (type == QLatin1String("regex"))
id = QMetaType::QRegularExpression;
else
id = QMetaType::type(type.toLatin1());
if (id == QMetaType::UnknownType) {
fprintf(stderr, "%lld:%lld: Invalid XML: unknown type '%s'.\n",
xml.lineNumber(), xml.columnNumber(), qPrintable(type.toString()));
exit(EXIT_FAILURE);
}
result = text.toString();
if (!result.convert(id)) {
fprintf(stderr, "%lld:%lld: Invalid XML: could not parse content as type '%s'.\n",
xml.lineNumber(), xml.columnNumber(), qPrintable(type.toString()));
exit(EXIT_FAILURE);
}
}
do {
xml.readNext();
} while (xml.isComment() || xml.isWhitespace());
if (!xml.isEndElement()) {
fprintf(stderr, "%lld:%lld: Invalid XML %s '%s'.\n",
xml.lineNumber(), xml.columnNumber(),
qPrintable(xml.tokenString()), qPrintable(name.toString()));
exit(EXIT_FAILURE);
}
xml.readNext();
return result;
}
static void variantToXml(QXmlStreamWriter &xml, const QVariant &v)
{
int type = v.userType();
if (type == QMetaType::QVariantList) {
QVariantList list = v.toList();
xml.writeStartElement("list");
for (const QVariant &v : list)
variantToXml(xml, v);
xml.writeEndElement();
} else if (type == QMetaType::QVariantMap || type == qMetaTypeId<VariantOrderedMap>()) {
const VariantOrderedMap map = (type == QMetaType::QVariantMap) ?
VariantOrderedMap(v.toMap()) :
qvariant_cast<VariantOrderedMap>(v);
xml.writeStartElement("map");
for (const auto &pair : map) {
xml.writeStartElement("entry");
variantToXml(xml, pair.first);
variantToXml(xml, pair.second);
xml.writeEndElement();
}
xml.writeEndElement();
} else {
xml.writeStartElement("value");
QString typeString = QStringLiteral("type");
switch (type) {
case QMetaType::Short:
case QMetaType::UShort:
case QMetaType::Int:
case QMetaType::UInt:
case QMetaType::Long:
case QMetaType::ULong:
case QMetaType::LongLong:
case QMetaType::ULongLong:
case QMetaType::Float:
case QMetaType::Double:
xml.writeAttribute(typeString, "number");
xml.writeCharacters(v.toString());
break;
case QMetaType::QByteArray:
xml.writeAttribute(typeString, "bytes");
xml.writeAttribute("encoding", "base64");
xml.writeCharacters(QString::fromLatin1(v.toByteArray().toBase64()));
break;
case QMetaType::QString:
xml.writeAttribute(typeString, "string");
xml.writeCDATA(v.toString());
break;
case QMetaType::Bool:
xml.writeAttribute(typeString, "bool");
xml.writeCharacters(v.toString());
break;
case QMetaType::Nullptr:
xml.writeAttribute(typeString, "null");
break;
case QMetaType::UnknownType:
break;
case QMetaType::QDate:
case QMetaType::QTime:
case QMetaType::QDateTime:
xml.writeAttribute(typeString, "dateime");
xml.writeCharacters(v.toString());
break;
case QMetaType::QUrl:
xml.writeAttribute(typeString, "url");
xml.writeCharacters(v.toUrl().toString(QUrl::FullyEncoded));
break;
case QMetaType::QUuid:
xml.writeAttribute(typeString, "uuid");
xml.writeCharacters(v.toString());
break;
case QMetaType::QBitArray:
xml.writeAttribute(typeString, "bits");
xml.writeCharacters([](const QBitArray &ba) {
QString result;
for (qsizetype i = 0; i < ba.size(); ++i) {
if (i && i % 72 == 0)
result += '\n';
result += QLatin1Char(ba.testBit(i) ? '1' : '0');
}
return result;
}(v.toBitArray()));
break;
case QMetaType::QRegularExpression:
xml.writeAttribute(typeString, "regex");
xml.writeCharacters(v.toRegularExpression().pattern());
break;
default:
if (type == qMetaTypeId<qfloat16>()) {
xml.writeAttribute(typeString, "number");
xml.writeCharacters(QString::number(float(qvariant_cast<qfloat16>(v))));
} else if (type == qMetaTypeId<QCborSimpleType>()) {
xml.writeAttribute(typeString, "CBOR simple type");
xml.writeCharacters(QString::number(int(qvariant_cast<QCborSimpleType>(v))));
} else {
// does this convert to string?
const char *typeName = v.typeName();
QVariant copy = v;
if (copy.convert(QMetaType::QString)) {
xml.writeAttribute(typeString, QString::fromLatin1(typeName));
xml.writeCharacters(copy.toString());
} else {
fprintf(stderr, "XML: don't know how to serialize type '%s'.\n", typeName);
exit(EXIT_FAILURE);
}
}
}
xml.writeEndElement();
}
}
QString XmlConverter::name()
{
return QStringLiteral("xml");
}
Converter::Direction XmlConverter::directions()
{
return InOut;
}
Converter::Options XmlConverter::outputOptions()
{
return SupportsArbitraryMapKeys;
}
const char *XmlConverter::optionsHelp()
{
return optionHelp;
}
bool XmlConverter::probeFile(QIODevice *f)
{
if (QFile *file = qobject_cast<QFile *>(f)) {
if (file->fileName().endsWith(QLatin1String(".xml")))
return true;
}
return f->isReadable() && f->peek(5) == "<?xml";
}
QVariant XmlConverter::loadFile(QIODevice *f, Converter *&outputConverter)
{
if (!outputConverter)
outputConverter = this;
QXmlStreamReader xml(f);
xml.readNextStartElement();
QVariant v = variantFromXml(xml, outputConverter->outputOptions());
if (xml.hasError()) {
fprintf(stderr, "XML error: %s", qPrintable(xml.errorString()));
exit(EXIT_FAILURE);
}
return v;
}
void XmlConverter::saveFile(QIODevice *f, const QVariant &contents, const QStringList &options)
{
bool compact = false;
for (const QString &s : options) {
if (s == QLatin1String("compact=no")) {
compact = false;
} else if (s == QLatin1String("compact=yes")) {
compact = true;
} else {
fprintf(stderr, "Unknown option '%s' to XML output. Valid options are:\n%s", qPrintable(s), optionHelp);
exit(EXIT_FAILURE);
}
}
QXmlStreamWriter xml(f);
xml.setAutoFormatting(!compact);
xml.writeStartDocument();
variantToXml(xml, contents);
xml.writeEndDocument();
}