1/****************************************************************************
2**
3** Copyright (C) 2018 Intel Corporation.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the examples of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:BSD$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** BSD License Usage
18** Alternatively, you may use this file under the terms of the BSD license
19** as follows:
20**
21** "Redistribution and use in source and binary forms, with or without
22** modification, are permitted provided that the following conditions are
23** met:
24** * Redistributions of source code must retain the above copyright
25** notice, this list of conditions and the following disclaimer.
26** * Redistributions in binary form must reproduce the above copyright
27** notice, this list of conditions and the following disclaimer in
28** the documentation and/or other materials provided with the
29** distribution.
30** * Neither the name of The Qt Company Ltd nor the names of its
31** contributors may be used to endorse or promote products derived
32** from this software without specific prior written permission.
33**
34**
35** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
36** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
37** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
38** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
39** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
40** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
41** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
42** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
43** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
44** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
45** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
46**
47** $QT_END_LICENSE$
48**
49****************************************************************************/
50
51#include "xmlconverter.h"
52
53#include <QBitArray>
54#include <QtCborCommon>
55#include <QFile>
56#include <QFloat16>
57#include <QMetaType>
58#include <QRegularExpression>
59#include <QUrl>
60#include <QXmlStreamReader>
61#include <QXmlStreamWriter>
62
63static const char optionHelp[] =
64 "compact=no|yes Use compact XML form.\n";
65
66static XmlConverter xmlConverter;
67
68static QVariant variantFromXml(QXmlStreamReader &xml, Converter::Options options);
69
70static QVariantList listFromXml(QXmlStreamReader &xml, Converter::Options options)
71{
72 QVariantList list;
73 while (!xml.atEnd() && !(xml.isEndElement() && xml.name() == QLatin1String("list"))) {
74 xml.readNext();
75 switch (xml.tokenType()) {
76 case QXmlStreamReader::StartElement:
77 list << variantFromXml(xml, options);
78 continue;
79
80 case QXmlStreamReader::EndElement:
81 continue;
82
83 case QXmlStreamReader::Comment:
84 // ignore comments
85 continue;
86
87 case QXmlStreamReader::Characters:
88 // ignore whitespace
89 if (xml.isWhitespace())
90 continue;
91 Q_FALLTHROUGH();
92
93 default:
94 break;
95 }
96
97 fprintf(stderr, "%lld:%lld: Invalid XML %s '%s'.\n",
98 xml.lineNumber(), xml.columnNumber(),
99 qPrintable(xml.tokenString()), qPrintable(xml.name().toString()));
100 exit(EXIT_FAILURE);
101 }
102
103 xml.readNext();
104 return list;
105}
106
107static VariantOrderedMap::value_type mapEntryFromXml(QXmlStreamReader &xml, Converter::Options options)
108{
109 QVariant key, value;
110 while (!xml.atEnd() && !(xml.isEndElement() && xml.name() == QLatin1String("entry"))) {
111 xml.readNext();
112 switch (xml.tokenType()) {
113 case QXmlStreamReader::StartElement:
114 if (value.isValid())
115 break;
116 if (key.isValid())
117 value = variantFromXml(xml, options);
118 else
119 key = variantFromXml(xml, options);
120 continue;
121
122 case QXmlStreamReader::EndElement:
123 continue;
124
125 case QXmlStreamReader::Comment:
126 // ignore comments
127 continue;
128
129 case QXmlStreamReader::Characters:
130 // ignore whitespace
131 if (xml.isWhitespace())
132 continue;
133 Q_FALLTHROUGH();
134
135 default:
136 break;
137 }
138
139 fprintf(stderr, "%lld:%lld: Invalid XML %s '%s'.\n",
140 xml.lineNumber(), xml.columnNumber(),
141 qPrintable(xml.tokenString()), qPrintable(xml.name().toString()));
142 exit(EXIT_FAILURE);
143 }
144
145 return { key, value };
146}
147
148static QVariant mapFromXml(QXmlStreamReader &xml, Converter::Options options)
149{
150 QVariantMap map1;
151 VariantOrderedMap map2;
152
153 while (!xml.atEnd() && !(xml.isEndElement() && xml.name() == QLatin1String("map"))) {
154 xml.readNext();
155 switch (xml.tokenType()) {
156 case QXmlStreamReader::StartElement:
157 if (xml.name() == QLatin1String("entry")) {
158 auto pair = mapEntryFromXml(xml, options);
159 if (options & Converter::SupportsArbitraryMapKeys)
160 map2.append(pair);
161 else
162 map1.insert(pair.first.toString(), pair.second);
163 continue;
164 }
165 break;
166
167 case QXmlStreamReader::EndElement:
168 continue;
169
170 case QXmlStreamReader::Comment:
171 // ignore comments
172 continue;
173
174 case QXmlStreamReader::Characters:
175 // ignore whitespace
176 if (xml.isWhitespace())
177 continue;
178 Q_FALLTHROUGH();
179
180 default:
181 break;
182 }
183
184 fprintf(stderr, "%lld:%lld: Invalid XML %s '%s'.\n",
185 xml.lineNumber(), xml.columnNumber(),
186 qPrintable(xml.tokenString()), qPrintable(xml.name().toString()));
187 exit(EXIT_FAILURE);
188 }
189
190 xml.readNext();
191 if (options & Converter::SupportsArbitraryMapKeys)
192 return QVariant::fromValue(map2);
193 return map1;
194}
195
196static QVariant variantFromXml(QXmlStreamReader &xml, Converter::Options options)
197{
198 QStringView name = xml.name();
199 if (name == QLatin1String("list"))
200 return listFromXml(xml, options);
201 if (name == QLatin1String("map"))
202 return mapFromXml(xml, options);
203 if (name != QLatin1String("value")) {
204 fprintf(stderr, "%lld:%lld: Invalid XML key '%s'.\n",
205 xml.lineNumber(), xml.columnNumber(), qPrintable(name.toString()));
206 exit(EXIT_FAILURE);
207 }
208
209 QXmlStreamAttributes attrs = xml.attributes();
210 QStringView type = attrs.value(QLatin1String("type"));
211
212 forever {
213 xml.readNext();
214 if (xml.isComment())
215 continue;
216 if (xml.isCDATA() || xml.isCharacters() || xml.isEndElement())
217 break;
218
219 fprintf(stderr, "%lld:%lld: Invalid XML %s '%s'.\n",
220 xml.lineNumber(), xml.columnNumber(),
221 qPrintable(xml.tokenString()), qPrintable(name.toString()));
222 exit(EXIT_FAILURE);
223 }
224
225 QStringView text = xml.text();
226 if (!xml.isCDATA())
227 text = text.trimmed();
228
229 QVariant result;
230 bool ok;
231 if (type.isEmpty()) {
232 // ok
233 } else if (type == QLatin1String("number")) {
234 // try integer first
235 qint64 v = text.toLongLong(&ok);
236 if (ok) {
237 result = v;
238 } else {
239 // let's see floating point
240 double d = text.toDouble(&ok);
241 result = d;
242 if (!ok) {
243 fprintf(stderr, "%lld:%lld: Invalid XML: could not interpret '%s' as a number.\n",
244 xml.lineNumber(), xml.columnNumber(), qPrintable(text.toString()));
245 exit(EXIT_FAILURE);
246 }
247 }
248 } else if (type == QLatin1String("bytes")) {
249 QByteArray data = text.toLatin1();
250 QStringView encoding = attrs.value("encoding");
251 if (encoding == QLatin1String("base64url")) {
252 result = QByteArray::fromBase64(data, QByteArray::Base64UrlEncoding);
253 } else if (encoding == QLatin1String("hex")) {
254 result = QByteArray::fromHex(data);
255 } else if (encoding.isEmpty() || encoding == QLatin1String("base64")) {
256 result = QByteArray::fromBase64(data);
257 } else {
258 fprintf(stderr, "%lld:%lld: Invalid XML: unknown encoding '%s' for bytes.\n",
259 xml.lineNumber(), xml.columnNumber(), qPrintable(encoding.toString()));
260 exit(EXIT_FAILURE);
261 }
262 } else if (type == QLatin1String("string")) {
263 result = text.toString();
264 } else if (type == QLatin1String("null")) {
265 result = QVariant::fromValue(nullptr);
266 } else if (type == QLatin1String("CBOR simple type")) {
267 result = QVariant::fromValue(QCborSimpleType(text.toShort()));
268 } else if (type == QLatin1String("bits")) {
269 QBitArray ba;
270 ba.resize(text.size());
271 qsizetype n = 0;
272 for (qsizetype i = 0; i < text.size(); ++i) {
273 QChar c = text.at(i);
274 if (c == '1') {
275 ba.setBit(n++);
276 } else if (c == '0') {
277 ++n;
278 } else if (!c.isSpace()) {
279 fprintf(stderr, "%lld:%lld: Invalid XML: invalid bit string '%s'.\n",
280 xml.lineNumber(), xml.columnNumber(), qPrintable(text.toString()));
281 exit(EXIT_FAILURE);
282 }
283 }
284 ba.resize(n);
285 result = ba;
286 } else {
287 int id = QMetaType::UnknownType;
288 if (type == QLatin1String("datetime"))
289 id = QMetaType::QDateTime;
290 else if (type == QLatin1String("url"))
291 id = QMetaType::QUrl;
292 else if (type == QLatin1String("uuid"))
293 id = QMetaType::QUuid;
294 else if (type == QLatin1String("regex"))
295 id = QMetaType::QRegularExpression;
296 else
297 id = QMetaType::fromName(type.toLatin1()).id();
298 if (id == QMetaType::UnknownType) {
299 fprintf(stderr, "%lld:%lld: Invalid XML: unknown type '%s'.\n",
300 xml.lineNumber(), xml.columnNumber(), qPrintable(type.toString()));
301 exit(EXIT_FAILURE);
302 }
303
304 result = text.toString();
305 if (!result.convert(QMetaType(id))) {
306 fprintf(stderr, "%lld:%lld: Invalid XML: could not parse content as type '%s'.\n",
307 xml.lineNumber(), xml.columnNumber(), qPrintable(type.toString()));
308 exit(EXIT_FAILURE);
309 }
310 }
311
312 do {
313 xml.readNext();
314 } while (xml.isComment() || xml.isWhitespace());
315
316 if (!xml.isEndElement()) {
317 fprintf(stderr, "%lld:%lld: Invalid XML %s '%s'.\n",
318 xml.lineNumber(), xml.columnNumber(),
319 qPrintable(xml.tokenString()), qPrintable(name.toString()));
320 exit(EXIT_FAILURE);
321 }
322
323 xml.readNext();
324 return result;
325}
326
327static void variantToXml(QXmlStreamWriter &xml, const QVariant &v)
328{
329 int type = v.userType();
330 if (type == QMetaType::QVariantList) {
331 QVariantList list = v.toList();
332 xml.writeStartElement("list");
333 for (const QVariant &v : list)
334 variantToXml(xml, v);
335 xml.writeEndElement();
336 } else if (type == QMetaType::QVariantMap || type == qMetaTypeId<VariantOrderedMap>()) {
337 const VariantOrderedMap map = (type == QMetaType::QVariantMap) ?
338 VariantOrderedMap(v.toMap()) :
339 qvariant_cast<VariantOrderedMap>(v);
340
341 xml.writeStartElement("map");
342 for (const auto &pair : map) {
343 xml.writeStartElement("entry");
344 variantToXml(xml, pair.first);
345 variantToXml(xml, pair.second);
346 xml.writeEndElement();
347 }
348 xml.writeEndElement();
349 } else {
350 xml.writeStartElement("value");
351 QString typeString = QStringLiteral("type");
352 switch (type) {
353 case QMetaType::Short:
354 case QMetaType::UShort:
355 case QMetaType::Int:
356 case QMetaType::UInt:
357 case QMetaType::Long:
358 case QMetaType::ULong:
359 case QMetaType::LongLong:
360 case QMetaType::ULongLong:
361 case QMetaType::Float:
362 case QMetaType::Double:
363 xml.writeAttribute(typeString, "number");
364 xml.writeCharacters(v.toString());
365 break;
366
367 case QMetaType::QByteArray:
368 xml.writeAttribute(typeString, "bytes");
369 xml.writeAttribute("encoding", "base64");
370 xml.writeCharacters(QString::fromLatin1(v.toByteArray().toBase64()));
371 break;
372
373 case QMetaType::QString:
374 xml.writeAttribute(typeString, "string");
375 xml.writeCDATA(v.toString());
376 break;
377
378 case QMetaType::Bool:
379 xml.writeAttribute(typeString, "bool");
380 xml.writeCharacters(v.toString());
381 break;
382
383 case QMetaType::Nullptr:
384 xml.writeAttribute(typeString, "null");
385 break;
386
387 case QMetaType::UnknownType:
388 break;
389
390 case QMetaType::QDate:
391 case QMetaType::QTime:
392 case QMetaType::QDateTime:
393 xml.writeAttribute(typeString, "dateime");
394 xml.writeCharacters(v.toString());
395 break;
396
397 case QMetaType::QUrl:
398 xml.writeAttribute(typeString, "url");
399 xml.writeCharacters(v.toUrl().toString(QUrl::FullyEncoded));
400 break;
401
402 case QMetaType::QUuid:
403 xml.writeAttribute(typeString, "uuid");
404 xml.writeCharacters(v.toString());
405 break;
406
407 case QMetaType::QBitArray:
408 xml.writeAttribute(typeString, "bits");
409 xml.writeCharacters([](const QBitArray &ba) {
410 QString result;
411 for (qsizetype i = 0; i < ba.size(); ++i) {
412 if (i && i % 72 == 0)
413 result += '\n';
414 result += QLatin1Char(ba.testBit(i) ? '1' : '0');
415 }
416 return result;
417 }(v.toBitArray()));
418 break;
419
420 case QMetaType::QRegularExpression:
421 xml.writeAttribute(typeString, "regex");
422 xml.writeCharacters(v.toRegularExpression().pattern());
423 break;
424
425 default:
426 if (type == qMetaTypeId<qfloat16>()) {
427 xml.writeAttribute(typeString, "number");
428 xml.writeCharacters(QString::number(float(qvariant_cast<qfloat16>(v))));
429 } else if (type == qMetaTypeId<QCborSimpleType>()) {
430 xml.writeAttribute(typeString, "CBOR simple type");
431 xml.writeCharacters(QString::number(int(qvariant_cast<QCborSimpleType>(v))));
432 } else {
433 // does this convert to string?
434 const char *typeName = v.typeName();
435 QVariant copy = v;
436 if (copy.convert(QMetaType(QMetaType::QString))) {
437 xml.writeAttribute(typeString, QString::fromLatin1(typeName));
438 xml.writeCharacters(copy.toString());
439 } else {
440 fprintf(stderr, "XML: don't know how to serialize type '%s'.\n", typeName);
441 exit(EXIT_FAILURE);
442 }
443 }
444 }
445 xml.writeEndElement();
446 }
447}
448
449QString XmlConverter::name()
450{
451 return QStringLiteral("xml");
452}
453
454Converter::Direction XmlConverter::directions()
455{
456 return InOut;
457}
458
459Converter::Options XmlConverter::outputOptions()
460{
461 return SupportsArbitraryMapKeys;
462}
463
464const char *XmlConverter::optionsHelp()
465{
466 return optionHelp;
467}
468
469bool XmlConverter::probeFile(QIODevice *f)
470{
471 if (QFile *file = qobject_cast<QFile *>(f)) {
472 if (file->fileName().endsWith(QLatin1String(".xml")))
473 return true;
474 }
475
476 return f->isReadable() && f->peek(5) == "<?xml";
477}
478
479QVariant XmlConverter::loadFile(QIODevice *f, Converter *&outputConverter)
480{
481 if (!outputConverter)
482 outputConverter = this;
483
484 QXmlStreamReader xml(f);
485 xml.readNextStartElement();
486 QVariant v = variantFromXml(xml, outputConverter->outputOptions());
487 if (xml.hasError()) {
488 fprintf(stderr, "XML error: %s", qPrintable(xml.errorString()));
489 exit(EXIT_FAILURE);
490 }
491
492 return v;
493}
494
495void XmlConverter::saveFile(QIODevice *f, const QVariant &contents, const QStringList &options)
496{
497 bool compact = false;
498 for (const QString &s : options) {
499 if (s == QLatin1String("compact=no")) {
500 compact = false;
501 } else if (s == QLatin1String("compact=yes")) {
502 compact = true;
503 } else {
504 fprintf(stderr, "Unknown option '%s' to XML output. Valid options are:\n%s", qPrintable(s), optionHelp);
505 exit(EXIT_FAILURE);
506 }
507 }
508
509 QXmlStreamWriter xml(f);
510 xml.setAutoFormatting(!compact);
511 xml.writeStartDocument();
512 variantToXml(xml, contents);
513 xml.writeEndDocument();
514}
515