1 | // Copyright (C) 2018 Intel Corporation. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include "qcborvalue.h" |
5 | #include "qcborvalue_p.h" |
6 | |
7 | #include "qcborarray.h" |
8 | #include "qcbormap.h" |
9 | |
10 | #include <private/qnumeric_p.h> |
11 | #include <qstack.h> |
12 | #include <private/qtools_p.h> |
13 | |
14 | QT_BEGIN_NAMESPACE |
15 | |
16 | using namespace Qt::StringLiterals; |
17 | |
18 | namespace { |
19 | class DiagnosticNotation |
20 | { |
21 | public: |
22 | static QString create(const QCborValue &v, QCborValue::DiagnosticNotationOptions opts) |
23 | { |
24 | DiagnosticNotation dn(opts); |
25 | dn.appendValue(v); |
26 | return dn.result; |
27 | } |
28 | |
29 | private: |
30 | QStack<int> byteArrayFormatStack; |
31 | QString separator; |
32 | QString result; |
33 | QCborValue::DiagnosticNotationOptions opts; |
34 | int nestingLevel = 0; |
35 | |
36 | struct Nest { |
37 | enum { IndentationWidth = 4 }; |
38 | DiagnosticNotation *dn; |
39 | Nest(DiagnosticNotation *that) : dn(that) |
40 | { |
41 | ++dn->nestingLevel; |
42 | static const char indent[IndentationWidth + 1] = " " ; |
43 | if (dn->opts & QCborValue::LineWrapped) |
44 | dn->separator += QLatin1StringView(indent, IndentationWidth); |
45 | } |
46 | ~Nest() |
47 | { |
48 | --dn->nestingLevel; |
49 | if (dn->opts & QCborValue::LineWrapped) |
50 | dn->separator.chop(n: IndentationWidth); |
51 | } |
52 | }; |
53 | |
54 | DiagnosticNotation(QCborValue::DiagnosticNotationOptions opts_) |
55 | : separator(opts_ & QCborValue::LineWrapped ? "\n"_L1 : ""_L1 ), opts(opts_) |
56 | { |
57 | byteArrayFormatStack.push(t: int(QCborKnownTags::ExpectedBase16)); |
58 | } |
59 | |
60 | void appendString(const QString &s); |
61 | void appendArray(const QCborArray &a); |
62 | void appendMap(const QCborMap &m); |
63 | void appendValue(const QCborValue &v); |
64 | }; |
65 | } |
66 | |
67 | static QString makeFpString(double d) |
68 | { |
69 | QString s; |
70 | quint64 v; |
71 | if (qt_is_inf(d)) { |
72 | s = (d < 0) ? QStringLiteral("-inf" ) : QStringLiteral("inf" ); |
73 | } else if (qt_is_nan(d)) { |
74 | s = QStringLiteral("nan" ); |
75 | } else if (convertDoubleTo(v: d, value: &v)) { |
76 | s = QString::fromLatin1(ba: "%1.0" ).arg(a: v); |
77 | if (d < 0) |
78 | s.prepend(c: u'-'); |
79 | } else { |
80 | s = QString::number(d, format: 'g', precision: QLocale::FloatingPointShortest); |
81 | if (!s.contains(c: u'.') && !s.contains(c: u'e')) |
82 | s += u'.'; |
83 | } |
84 | return s; |
85 | } |
86 | |
87 | static bool isByteArrayEncodingTag(QCborTag tag) |
88 | { |
89 | switch (quint64(tag)) { |
90 | case quint64(QCborKnownTags::ExpectedBase16): |
91 | case quint64(QCborKnownTags::ExpectedBase64): |
92 | case quint64(QCborKnownTags::ExpectedBase64url): |
93 | return true; |
94 | } |
95 | return false; |
96 | } |
97 | |
98 | void DiagnosticNotation::appendString(const QString &s) |
99 | { |
100 | result += u'"'; |
101 | |
102 | const QChar *begin = s.begin(); |
103 | const QChar *end = s.end(); |
104 | while (begin < end) { |
105 | // find the longest span comprising only non-escaped characters |
106 | const QChar *ptr = begin; |
107 | for ( ; ptr < end; ++ptr) { |
108 | ushort uc = ptr->unicode(); |
109 | if (uc == '\\' || uc == '"' || uc < ' ' || uc >= 0x7f) |
110 | break; |
111 | } |
112 | |
113 | if (ptr != begin) |
114 | result.append(uc: begin, len: ptr - begin); |
115 | |
116 | if (ptr == end) |
117 | break; |
118 | |
119 | // there's an escaped character |
120 | static const char escapeMap[16] = { |
121 | // The C escape characters \a \b \t \n \v \f and \r indexed by |
122 | // their ASCII values |
123 | 0, 0, 0, 0, |
124 | 0, 0, 0, 'a', |
125 | 'b', 't', 'n', 'v', |
126 | 'f', 'r', 0, 0 |
127 | }; |
128 | int buflen = 2; |
129 | QChar buf[10]; |
130 | buf[0] = u'\\'; |
131 | buf[1] = QChar::Null; |
132 | char16_t uc = ptr->unicode(); |
133 | |
134 | if (uc < sizeof(escapeMap)) |
135 | buf[1] = QLatin1Char(escapeMap[uc]); |
136 | else if (uc == '"' || uc == '\\') |
137 | buf[1] = QChar(uc); |
138 | |
139 | if (buf[1] == QChar::Null) { |
140 | const auto toHexUpper = [](char32_t value) -> QChar { |
141 | // QtMiscUtils::toHexUpper() returns char, we need QChar, so wrap |
142 | return char16_t(QtMiscUtils::toHexUpper(value)); |
143 | }; |
144 | if (ptr->isHighSurrogate() && (ptr + 1) != end && ptr[1].isLowSurrogate()) { |
145 | // properly-paired surrogates |
146 | ++ptr; |
147 | char32_t ucs4 = QChar::surrogateToUcs4(high: uc, low: ptr->unicode()); |
148 | buf[1] = u'U'; |
149 | buf[2] = u'0'; // toHexUpper(ucs4 >> 28); |
150 | buf[3] = u'0'; // toHexUpper(ucs4 >> 24); |
151 | buf[4] = toHexUpper(ucs4 >> 20); |
152 | buf[5] = toHexUpper(ucs4 >> 16); |
153 | buf[6] = toHexUpper(ucs4 >> 12); |
154 | buf[7] = toHexUpper(ucs4 >> 8); |
155 | buf[8] = toHexUpper(ucs4 >> 4); |
156 | buf[9] = toHexUpper(ucs4); |
157 | buflen = 10; |
158 | } else { |
159 | buf[1] = u'u'; |
160 | buf[2] = toHexUpper(uc >> 12); |
161 | buf[3] = toHexUpper(uc >> 8); |
162 | buf[4] = toHexUpper(uc >> 4); |
163 | buf[5] = toHexUpper(uc); |
164 | buflen = 6; |
165 | } |
166 | } |
167 | |
168 | result.append(uc: buf, len: buflen); |
169 | begin = ptr + 1; |
170 | } |
171 | |
172 | result += u'"'; |
173 | } |
174 | |
175 | void DiagnosticNotation::appendArray(const QCborArray &a) |
176 | { |
177 | result += u'['; |
178 | |
179 | // length 2 (including the space) when not line wrapping |
180 | QLatin1StringView commaValue(", " , opts & QCborValue::LineWrapped ? 1 : 2); |
181 | { |
182 | Nest n(this); |
183 | QLatin1StringView comma; |
184 | for (auto v : a) { |
185 | result += comma + separator; |
186 | comma = commaValue; |
187 | appendValue(v); |
188 | } |
189 | } |
190 | |
191 | result += separator + u']'; |
192 | } |
193 | |
194 | void DiagnosticNotation::appendMap(const QCborMap &m) |
195 | { |
196 | result += u'{'; |
197 | |
198 | // length 2 (including the space) when not line wrapping |
199 | QLatin1StringView commaValue(", " , opts & QCborValue::LineWrapped ? 1 : 2); |
200 | { |
201 | Nest n(this); |
202 | QLatin1StringView comma; |
203 | for (auto v : m) { |
204 | result += comma + separator; |
205 | comma = commaValue; |
206 | appendValue(v: v.first); |
207 | result += ": "_L1 ; |
208 | appendValue(v: v.second); |
209 | } |
210 | } |
211 | |
212 | result += separator + u'}'; |
213 | }; |
214 | |
215 | void DiagnosticNotation::appendValue(const QCborValue &v) |
216 | { |
217 | switch (v.type()) { |
218 | case QCborValue::Integer: |
219 | result += QString::number(v.toInteger()); |
220 | return; |
221 | case QCborValue::ByteArray: |
222 | switch (byteArrayFormatStack.top()) { |
223 | case int(QCborKnownTags::ExpectedBase16): |
224 | result += QString::fromLatin1(ba: "h'" + |
225 | v.toByteArray().toHex(separator: opts & QCborValue::ExtendedFormat ? ' ' : '\0') + |
226 | '\''); |
227 | return; |
228 | case int(QCborKnownTags::ExpectedBase64): |
229 | result += QString::fromLatin1(ba: "b64'" + v.toByteArray().toBase64() + '\''); |
230 | return; |
231 | default: |
232 | case int(QCborKnownTags::ExpectedBase64url): |
233 | result += QString::fromLatin1(ba: "b64'" + |
234 | v.toByteArray().toBase64(options: QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + |
235 | '\''); |
236 | return; |
237 | } |
238 | case QCborValue::String: |
239 | return appendString(s: v.toString()); |
240 | case QCborValue::Array: |
241 | return appendArray(a: v.toArray()); |
242 | case QCborValue::Map: |
243 | return appendMap(m: v.toMap()); |
244 | case QCborValue::False: |
245 | result += "false"_L1 ; |
246 | return; |
247 | case QCborValue::True: |
248 | result += "true"_L1 ; |
249 | return; |
250 | case QCborValue::Null: |
251 | result += "null"_L1 ; |
252 | return; |
253 | case QCborValue::Undefined: |
254 | result += "undefined"_L1 ; |
255 | return; |
256 | case QCborValue::Double: |
257 | result += makeFpString(d: v.toDouble()); |
258 | return; |
259 | case QCborValue::Invalid: |
260 | result += QStringLiteral("<invalid>" ); |
261 | return; |
262 | |
263 | default: |
264 | // Only tags, extended types, and simple types remain; see below. |
265 | break; |
266 | } |
267 | |
268 | if (v.isTag()) { |
269 | // We handle all extended types as regular tags, so it won't matter |
270 | // whether we understand that tag or not. |
271 | bool byteArrayFormat = opts & QCborValue::ExtendedFormat && isByteArrayEncodingTag(tag: v.tag()); |
272 | if (byteArrayFormat) |
273 | byteArrayFormatStack.push(t: int(v.tag())); |
274 | result += QString::number(quint64(v.tag())) + u'('; |
275 | appendValue(v: v.taggedValue()); |
276 | result += u')'; |
277 | if (byteArrayFormat) |
278 | byteArrayFormatStack.pop(); |
279 | } else { |
280 | // must be a simple type |
281 | result += QString::fromLatin1(ba: "simple(%1)" ).arg(a: quint8(v.toSimpleType())); |
282 | } |
283 | } |
284 | |
285 | /*! |
286 | Creates the diagnostic notation equivalent of this CBOR object and returns |
287 | it. The \a opts parameter controls the dialect of the notation. Diagnostic |
288 | notation is useful in debugging, to aid the developer in understanding what |
289 | value is stored in the QCborValue or in a CBOR stream. For that reason, the |
290 | Qt API provides no support for parsing the diagnostic back into the |
291 | in-memory format or CBOR stream, though the representation is unique and it |
292 | would be possible. |
293 | |
294 | CBOR diagnostic notation is specified by |
295 | \l{RFC 7049, section 6}{section 6} of RFC 7049. |
296 | It is a text representation of the CBOR stream and it is very similar to |
297 | JSON, but it supports the CBOR types not found in JSON. The extended format |
298 | enabled by the \l{DiagnosticNotationOption}{ExtendedFormat} flag is |
299 | currently in some IETF drafts and its format is subject to change. |
300 | |
301 | This function produces the equivalent representation of the stream that |
302 | toCbor() would produce, without any transformation option provided there. |
303 | This also implies this function may not produce a representation of the |
304 | stream that was used to create the object, if it was created using |
305 | fromCbor(), as that function may have applied transformations. For a |
306 | high-fidelity notation of a stream, without transformation, see the \c |
307 | cbordump example. |
308 | |
309 | \sa toCbor(), QJsonDocument::toJson() |
310 | */ |
311 | QString QCborValue::toDiagnosticNotation(DiagnosticNotationOptions opts) const |
312 | { |
313 | return DiagnosticNotation::create(v: *this, opts); |
314 | } |
315 | |
316 | QT_END_NAMESPACE |
317 | |