// Copyright (C) 2018 Intel Corporation. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause #include "xmlconverter.h" #include "variantorderedmap.h" #include #include #include #include #include #include #include #include #include using namespace Qt::StringLiterals; static const char xmlOptionHelp[] = "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() == "list"_L1)) { 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; } qFatal("%lld:%lld: Invalid XML %s '%s'.", xml.lineNumber(), xml.columnNumber(), qPrintable(xml.tokenString()), qPrintable(xml.name().toString())); } xml.readNext(); return list; } static VariantOrderedMap::value_type mapEntryFromXml(QXmlStreamReader &xml, Converter::Options options) { QVariant key, value; while (!xml.atEnd() && !(xml.isEndElement() && xml.name() == "entry"_L1)) { 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; } qFatal("%lld:%lld: Invalid XML %s '%s'.", xml.lineNumber(), xml.columnNumber(), qPrintable(xml.tokenString()), qPrintable(xml.name().toString())); } return { key, value }; } static QVariant mapFromXml(QXmlStreamReader &xml, Converter::Options options) { QVariantMap map1; VariantOrderedMap map2; while (!xml.atEnd() && !(xml.isEndElement() && xml.name() == "map"_L1)) { xml.readNext(); switch (xml.tokenType()) { case QXmlStreamReader::StartElement: if (xml.name() == "entry"_L1) { 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; } qFatal("%lld:%lld: Invalid XML %s '%s'.", xml.lineNumber(), xml.columnNumber(), qPrintable(xml.tokenString()), qPrintable(xml.name().toString())); } xml.readNext(); if (options & Converter::SupportsArbitraryMapKeys) return QVariant::fromValue(map2); return map1; } static QVariant variantFromXml(QXmlStreamReader &xml, Converter::Options options) { QStringView name = xml.name(); if (name == "list"_L1) return listFromXml(xml, options); if (name == "map"_L1) return mapFromXml(xml, options); if (name != "value"_L1) { qFatal("%lld:%lld: Invalid XML key '%s'.", xml.lineNumber(), xml.columnNumber(), qPrintable(name.toString())); } QXmlStreamAttributes attrs = xml.attributes(); QStringView type = attrs.value("type"_L1); forever { xml.readNext(); if (xml.isComment()) continue; if (xml.isCDATA() || xml.isCharacters() || xml.isEndElement()) break; qFatal("%lld:%lld: Invalid XML %s '%s'.", xml.lineNumber(), xml.columnNumber(), qPrintable(xml.tokenString()), qPrintable(name.toString())); } QStringView text = xml.text(); if (!xml.isCDATA()) text = text.trimmed(); QVariant result; if (type.isEmpty()) { // ok } else if (type == "number"_L1) { // try integer first bool ok; qint64 v = text.toLongLong(&ok); if (ok) { result = v; } else { // let's see floating point double d = text.toDouble(&ok); if (!ok) { qFatal("%lld:%lld: Invalid XML: could not interpret '%s' as a number.", xml.lineNumber(), xml.columnNumber(), qPrintable(text.toString())); } result = d; } } else if (type == "bytes"_L1) { QByteArray data = text.toLatin1(); QStringView encoding = attrs.value("encoding"); if (encoding == "base64url"_L1) { result = QByteArray::fromBase64(data, QByteArray::Base64UrlEncoding); } else if (encoding == "hex"_L1) { result = QByteArray::fromHex(data); } else if (encoding.isEmpty() || encoding == "base64"_L1) { result = QByteArray::fromBase64(data); } else { qFatal("%lld:%lld: Invalid XML: unknown encoding '%s' for bytes.", xml.lineNumber(), xml.columnNumber(), qPrintable(encoding.toString())); } } else if (type == "string"_L1) { result = text.toString(); } else if (type == "null"_L1) { result = QVariant::fromValue(nullptr); } else if (type == "CBOR simple type"_L1) { result = QVariant::fromValue(QCborSimpleType(text.toShort())); } else if (type == "bits"_L1) { 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()) { qFatal("%lld:%lld: Invalid XML: invalid bit string '%s'.", xml.lineNumber(), xml.columnNumber(), qPrintable(text.toString())); } } ba.resize(n); result = ba; } else { int id = QMetaType::UnknownType; if (type == "datetime"_L1) id = QMetaType::QDateTime; else if (type == "url"_L1) id = QMetaType::QUrl; else if (type == "uuid"_L1) id = QMetaType::QUuid; else if (type == "regex"_L1) id = QMetaType::QRegularExpression; else id = QMetaType::fromName(type.toLatin1()).id(); if (id == QMetaType::UnknownType) { qFatal("%lld:%lld: Invalid XML: unknown type '%s'.", xml.lineNumber(), xml.columnNumber(), qPrintable(type.toString())); } result = text.toString(); if (!result.convert(QMetaType(id))) { qFatal("%lld:%lld: Invalid XML: could not parse content as type '%s'.", xml.lineNumber(), xml.columnNumber(), qPrintable(type.toString())); } } do { xml.readNext(); } while (xml.isComment() || xml.isWhitespace()); if (!xml.isEndElement()) { qFatal("%lld:%lld: Invalid XML %s '%s'.", xml.lineNumber(), xml.columnNumber(), qPrintable(xml.tokenString()), qPrintable(name.toString())); } 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()) { const VariantOrderedMap map = (type == QMetaType::QVariantMap) ? VariantOrderedMap(v.toMap()) : qvariant_cast(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 = "type"_L1; 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()) { xml.writeAttribute(typeString, "number"); xml.writeCharacters(QString::number(float(qvariant_cast(v)))); } else if (type == qMetaTypeId()) { xml.writeAttribute(typeString, "CBOR simple type"); xml.writeCharacters(QString::number(int(qvariant_cast(v)))); } else { // does this convert to string? const char *typeName = v.typeName(); QVariant copy = v; if (copy.convert(QMetaType(QMetaType::QString))) { xml.writeAttribute(typeString, QString::fromLatin1(typeName)); xml.writeCharacters(copy.toString()); } else { qFatal("XML: don't know how to serialize type '%s'.", typeName); } } } xml.writeEndElement(); } } QString XmlConverter::name() const { return "xml"_L1; } Converter::Directions XmlConverter::directions() const { return Direction::InOut; } Converter::Options XmlConverter::outputOptions() const { return SupportsArbitraryMapKeys; } const char *XmlConverter::optionsHelp() const { return xmlOptionHelp; } bool XmlConverter::probeFile(QIODevice *f) const { if (QFile *file = qobject_cast(f)) { if (file->fileName().endsWith(".xml"_L1)) return true; } return f->isReadable() && f->peek(5) == "outputOptions()); if (xml.hasError()) qFatal("XML error: %s", qPrintable(xml.errorString())); return v; } void XmlConverter::saveFile(QIODevice *f, const QVariant &contents, const QStringList &options) const { bool compact = false; for (const QString &s : options) { if (s == "compact=no"_L1) { compact = false; } else if (s == "compact=yes"_L1) { compact = true; } else { qFatal("Unknown option '%s' to XML output. Valid options are:\n%s", qPrintable(s), xmlOptionHelp); } } QXmlStreamWriter xml(f); xml.setAutoFormatting(!compact); xml.writeStartDocument(); variantToXml(xml, contents); xml.writeEndDocument(); }