/src/swift-protobuf/Sources/SwiftProtobuf/AnyMessageStorage.swift
Line | Count | Source |
1 | | // Sources/SwiftProtobuf/AnyMessageStorage.swift - Custom storage for Any WKT |
2 | | // |
3 | | // Copyright (c) 2014 - 2017 Apple Inc. and the project authors |
4 | | // Licensed under Apache License v2.0 with Runtime Library Exception |
5 | | // |
6 | | // See LICENSE.txt for license information: |
7 | | // https://github.com/apple/swift-protobuf/blob/main/LICENSE.txt |
8 | | // |
9 | | // ----------------------------------------------------------------------------- |
10 | | /// |
11 | | /// Hand written storage class for Google_Protobuf_Any to support on demand |
12 | | /// transforms between the formats. |
13 | | /// |
14 | | // ----------------------------------------------------------------------------- |
15 | | |
16 | | #if canImport(FoundationEssentials) |
17 | | import FoundationEssentials |
18 | | #else |
19 | | import Foundation |
20 | | #endif |
21 | | |
22 | | private func serializeAnyJSON( |
23 | | for message: any Message, |
24 | | typeURL: String, |
25 | | options: JSONEncodingOptions |
26 | 0 | ) throws -> String { |
27 | 0 | var visitor = try JSONEncodingVisitor(type: type(of: message), options: options) |
28 | 0 | visitor.startObject(message: message) |
29 | 0 | visitor.encodeField(name: "@type", stringValue: typeURL) |
30 | 0 | if let m = message as? (any _CustomJSONCodable) { |
31 | 0 | let value = try m.encodedJSONString(options: options) |
32 | 0 | visitor.encodeField(name: "value", jsonText: value) |
33 | 0 | } else { |
34 | 0 | try message.traverse(visitor: &visitor) |
35 | 0 | } |
36 | 0 | visitor.endObject() |
37 | 0 | return visitor.stringResult |
38 | 0 | } |
39 | | |
40 | 202k | private func emitVerboseTextForm(visitor: inout TextFormatEncodingVisitor, message: any Message, typeURL: String) { |
41 | 202k | let url: String |
42 | 202k | if typeURL.isEmpty { |
43 | 0 | url = buildTypeURL(forMessage: message, typePrefix: defaultAnyTypeURLPrefix) |
44 | 202k | } else { |
45 | 202k | url = typeURL |
46 | 202k | } |
47 | 202k | visitor.visitAnyVerbose(value: message, typeURL: url) |
48 | 202k | } |
49 | | |
50 | 0 | private func asJSONObject(body: [UInt8]) -> Data { |
51 | 0 | let asciiOpenCurlyBracket = UInt8(ascii: "{") |
52 | 0 | let asciiCloseCurlyBracket = UInt8(ascii: "}") |
53 | 0 |
|
54 | 0 | var result = Data() |
55 | 0 | result.reserveCapacity(body.count + 2) |
56 | 0 | result.append(asciiOpenCurlyBracket) |
57 | 0 | result.append(contentsOf: body) |
58 | 0 | result.append(asciiCloseCurlyBracket) |
59 | 0 | return result |
60 | 0 | } |
61 | | |
62 | | private func unpack( |
63 | | contentJSON: [UInt8], |
64 | | extensions: any ExtensionMap, |
65 | | options: JSONDecodingOptions, |
66 | | as messageType: any Message.Type |
67 | 0 | ) throws -> any Message { |
68 | 0 | guard messageType is any _CustomJSONCodable.Type else { |
69 | 0 | let contentJSONAsObject = asJSONObject(body: contentJSON) |
70 | 0 | return try messageType.init(jsonUTF8Bytes: contentJSONAsObject, extensions: extensions, options: options) |
71 | 0 | } |
72 | 0 |
|
73 | 0 | var value = String() |
74 | 0 | try contentJSON.withUnsafeBytes { (body: UnsafeRawBufferPointer) in |
75 | 0 | if body.count > 0 { |
76 | 0 | // contentJSON will be the valid JSON for inside an object (everything but |
77 | 0 | // the '{' and '}', so minimal validation is needed. |
78 | 0 | var scanner = JSONScanner(source: body, options: options, extensions: extensions) |
79 | 0 | while !scanner.complete { |
80 | 0 | let key = try scanner.nextQuotedString() |
81 | 0 | try scanner.skipRequiredColon() |
82 | 0 | if key == "value" { |
83 | 0 | value = try scanner.skip() |
84 | 0 | break |
85 | 0 | } |
86 | 0 | if !options.ignoreUnknownFields { |
87 | 0 | // The only thing within a WKT should be "value". |
88 | 0 | throw AnyUnpackError.malformedWellKnownTypeJSON |
89 | 0 | } |
90 | 0 | let _ = try scanner.skip() |
91 | 0 | try scanner.skipRequiredComma() |
92 | 0 | } |
93 | 0 | if !options.ignoreUnknownFields && !scanner.complete { |
94 | 0 | // If that wasn't the end, then there was another key, and WKTs should |
95 | 0 | // only have the one when not skipping unknowns. |
96 | 0 | throw AnyUnpackError.malformedWellKnownTypeJSON |
97 | 0 | } |
98 | 0 | } |
99 | 0 | } |
100 | 0 | return try messageType.init(jsonString: value, extensions: extensions, options: options) |
101 | 0 | } |
102 | | |
103 | | internal class AnyMessageStorage { |
104 | | // The two properties generated Google_Protobuf_Any will reference. |
105 | 1.51M | var _typeURL = String() |
106 | | var _value: Data { |
107 | | // Remapped to the internal `state`. |
108 | 433k | get { |
109 | 433k | switch state { |
110 | 433k | case .binary(let value): |
111 | 433k | return value |
112 | 433k | case .message(let message): |
113 | 0 | do { |
114 | 0 | return try message.serializedBytes(partial: true) |
115 | 0 | } catch { |
116 | 0 | return Data() |
117 | 0 | } |
118 | 433k | case .contentJSON(let contentJSON, let options): |
119 | 0 | guard let messageType = Google_Protobuf_Any.messageType(forTypeURL: _typeURL) else { |
120 | 0 | return Data() |
121 | 0 | } |
122 | 0 | do { |
123 | 0 | let m = try unpack( |
124 | 0 | contentJSON: contentJSON, |
125 | 0 | extensions: SimpleExtensionMap(), |
126 | 0 | options: options, |
127 | 0 | as: messageType |
128 | 0 | ) |
129 | 0 | return try m.serializedBytes(partial: true) |
130 | 0 | } catch { |
131 | 0 | return Data() |
132 | 0 | } |
133 | 433k | } |
134 | 433k | } |
135 | 5.89M | set { |
136 | 5.89M | state = .binary(newValue) |
137 | 5.89M | } |
138 | | } |
139 | | |
140 | | enum InternalState { |
141 | | // a serialized binary |
142 | | // Note: Unlike contentJSON below, binary does not bother to capture the |
143 | | // decoding options. This is because the actual binary format is the binary |
144 | | // blob, i.e. - when decoding from binary, the spec doesn't include decoding |
145 | | // the binary blob, it is pass through. Instead there is a public api for |
146 | | // unpacking that takes new options when a developer decides to decode it. |
147 | | case binary(Data) |
148 | | // a message |
149 | | case message(any Message) |
150 | | // parsed JSON with the @type removed and the decoding options. |
151 | | case contentJSON([UInt8], JSONDecodingOptions) |
152 | | } |
153 | 1.51M | var state: InternalState = .binary(Data()) |
154 | | |
155 | | // This property is used as the initial default value for new instances of the type. |
156 | | // The type itself is protecting the reference to its storage via CoW semantics. |
157 | | // This will force a copy to be made of this reference when the first mutation occurs; |
158 | | // hence, it is safe to mark this as `nonisolated(unsafe)`. |
159 | | static nonisolated(unsafe) let defaultInstance = AnyMessageStorage() |
160 | | |
161 | 8 | private init() {} |
162 | | |
163 | 1.47M | init(copying source: AnyMessageStorage) { |
164 | 1.47M | _typeURL = source._typeURL |
165 | 1.47M | state = source.state |
166 | 1.47M | } |
167 | | |
168 | 0 | func isA<M: Message>(_ type: M.Type) -> Bool { |
169 | 0 | if _typeURL.isEmpty { |
170 | 0 | return false |
171 | 0 | } |
172 | 0 | let encodedType = typeName(fromURL: _typeURL) |
173 | 0 | return encodedType == M.protoMessageName |
174 | 0 | } |
175 | | |
176 | | // This is only ever called with the expectation that target will be fully |
177 | | // replaced during the unpacking and never as a merge. |
178 | | func unpackTo<M: Message>( |
179 | | target: inout M, |
180 | | extensions: (any ExtensionMap)?, |
181 | | options: BinaryDecodingOptions |
182 | 0 | ) throws { |
183 | 0 | guard isA(M.self) else { |
184 | 0 | throw AnyUnpackError.typeMismatch |
185 | 0 | } |
186 | 0 |
|
187 | 0 | switch state { |
188 | 0 | case .binary(let data): |
189 | 0 | target = try M(serializedBytes: data, extensions: extensions, partial: true, options: options) |
190 | 0 |
|
191 | 0 | case .message(let msg): |
192 | 0 | if let message = msg as? M { |
193 | 0 | // Already right type, copy it over. |
194 | 0 | target = message |
195 | 0 | } else { |
196 | 0 | // Different type, serialize and parse. |
197 | 0 | let bytes: [UInt8] = try msg.serializedBytes(partial: true) |
198 | 0 | target = try M(serializedBytes: bytes, extensions: extensions, partial: true) |
199 | 0 | } |
200 | 0 |
|
201 | 0 | case .contentJSON(let contentJSON, let options): |
202 | 0 | target = |
203 | 0 | try unpack( |
204 | 0 | contentJSON: contentJSON, |
205 | 0 | extensions: extensions ?? SimpleExtensionMap(), |
206 | 0 | options: options, |
207 | 0 | as: M.self |
208 | 0 | ) as! M |
209 | 0 | } |
210 | 0 | } |
211 | | |
212 | | // Called before the message is traversed to do any error preflights. |
213 | | // Since traverse() will use _value, this is our chance to throw |
214 | | // when _value can't. |
215 | 91.3k | func preTraverse() throws { |
216 | 91.3k | switch state { |
217 | 91.3k | case .binary: |
218 | 91.3k | // Nothing to be checked. |
219 | 91.3k | break |
220 | 91.3k | |
221 | 91.3k | case .message: |
222 | 0 | // When set from a developer provided message, partial support |
223 | 0 | // is done. Any message that comes in from another format isn't |
224 | 0 | // checked, and transcoding the isInitialized requirement is |
225 | 0 | // never inserted. |
226 | 0 | break |
227 | 91.3k | |
228 | 91.3k | case .contentJSON(let contentJSON, let options): |
229 | 0 | // contentJSON requires we have the type available for decoding. |
230 | 0 | guard let messageType = Google_Protobuf_Any.messageType(forTypeURL: _typeURL) else { |
231 | 0 | throw BinaryEncodingError.anyTranscodeFailure |
232 | 0 | } |
233 | 0 | do { |
234 | 0 | // Decodes the full JSON and then discard the result. |
235 | 0 | // The regular traversal will decode this again by querying the |
236 | 0 | // `value` field, but that has no way to fail. As a result, |
237 | 0 | // we need this to accurately handle decode errors. |
238 | 0 | _ = try unpack( |
239 | 0 | contentJSON: contentJSON, |
240 | 0 | extensions: SimpleExtensionMap(), |
241 | 0 | options: options, |
242 | 0 | as: messageType |
243 | 0 | ) |
244 | 0 | } catch { |
245 | 0 | throw BinaryEncodingError.anyTranscodeFailure |
246 | 0 | } |
247 | 91.3k | } |
248 | 91.3k | } |
249 | | } |
250 | | |
251 | | /// Custom handling for Text format. |
252 | | extension AnyMessageStorage { |
253 | 1.62k | func decodeTextFormat(typeURL url: String, decoder: inout TextFormatDecoder) throws { |
254 | 1.62k | // Decoding the verbose form requires knowing the type. |
255 | 1.62k | _typeURL = url |
256 | 1.62k | guard let messageType = Google_Protobuf_Any.messageType(forTypeURL: url) else { |
257 | 96 | // The type wasn't registered, can't parse it. |
258 | 96 | throw TextFormatDecodingError.malformedText |
259 | 1.52k | } |
260 | 1.52k | let terminator = try decoder.scanner.skipObjectStart() |
261 | 1.52k | var subDecoder = try TextFormatDecoder( |
262 | 1.52k | messageType: messageType, |
263 | 1.52k | scanner: decoder.scanner, |
264 | 1.52k | terminator: terminator |
265 | 1.52k | ) |
266 | 1.52k | if messageType == Google_Protobuf_Any.self { |
267 | 610 | var any = Google_Protobuf_Any() |
268 | 610 | try any.decodeTextFormat(decoder: &subDecoder) |
269 | 498 | state = .message(any) |
270 | 917 | } else { |
271 | 917 | var m = messageType.init() |
272 | 917 | try m.decodeMessage(decoder: &subDecoder) |
273 | 914 | state = .message(m) |
274 | 1.41k | } |
275 | 1.41k | decoder.scanner = subDecoder.scanner |
276 | 1.41k | if try decoder.nextFieldNumber() != nil { |
277 | 2 | // Verbose any can never have additional keys. |
278 | 2 | throw TextFormatDecodingError.malformedText |
279 | 1.37k | } |
280 | 1.37k | } |
281 | | |
282 | | // Specialized traverse for writing out a Text form of the Any. |
283 | | // This prefers the more-legible "verbose" format if it can |
284 | | // use it, otherwise will fall back to simpler forms. |
285 | 321k | internal func textTraverse(visitor: inout TextFormatEncodingVisitor) { |
286 | 321k | switch state { |
287 | 321k | case .binary(let valueData): |
288 | 321k | if let messageType = Google_Protobuf_Any.messageType(forTypeURL: _typeURL) { |
289 | 272k | // If we can decode it, we can write the readable verbose form: |
290 | 272k | do { |
291 | 272k | let m = try messageType.init(serializedBytes: valueData, partial: true) |
292 | 195k | emitVerboseTextForm(visitor: &visitor, message: m, typeURL: _typeURL) |
293 | 195k | return |
294 | 272k | } catch { |
295 | 77.6k | // Fall through to just print the type and raw binary data. |
296 | 77.6k | } |
297 | 126k | } |
298 | 126k | if !_typeURL.isEmpty { |
299 | 102k | try! visitor.visitSingularStringField(value: _typeURL, fieldNumber: 1) |
300 | 126k | } |
301 | 126k | if !valueData.isEmpty { |
302 | 91.4k | try! visitor.visitSingularBytesField(value: valueData, fieldNumber: 2) |
303 | 91.4k | } |
304 | 321k | |
305 | 321k | case .message(let msg): |
306 | 212 | emitVerboseTextForm(visitor: &visitor, message: msg, typeURL: _typeURL) |
307 | 321k | |
308 | 321k | case .contentJSON(let contentJSON, let options): |
309 | 0 | // If we can decode it, we can write the readable verbose form: |
310 | 0 | if let messageType = Google_Protobuf_Any.messageType(forTypeURL: _typeURL) { |
311 | 0 | do { |
312 | 0 | let m = try unpack( |
313 | 0 | contentJSON: contentJSON, |
314 | 0 | extensions: SimpleExtensionMap(), |
315 | 0 | options: options, |
316 | 0 | as: messageType |
317 | 0 | ) |
318 | 0 | emitVerboseTextForm(visitor: &visitor, message: m, typeURL: _typeURL) |
319 | 0 | return |
320 | 0 | } catch { |
321 | 0 | // Fall through to just print the raw JSON data |
322 | 0 | } |
323 | 0 | } |
324 | 0 | if !_typeURL.isEmpty { |
325 | 0 | try! visitor.visitSingularStringField(value: _typeURL, fieldNumber: 1) |
326 | 0 | } |
327 | 0 | // Build a readable form of the JSON: |
328 | 0 | let contentJSONAsObject = asJSONObject(body: contentJSON) |
329 | 0 | visitor.visitAnyJSONBytesField(value: contentJSONAsObject) |
330 | 321k | } |
331 | 126k | } |
332 | | } |
333 | | |
334 | | /// The obvious goal for Hashable/Equatable conformance would be for |
335 | | /// hash and equality to behave as if we always decoded the inner |
336 | | /// object and hashed or compared that. Unfortunately, Any typically |
337 | | /// stores serialized contents and we don't always have the ability to |
338 | | /// deserialize it. Since none of our supported serializations are |
339 | | /// fully deterministic, we can't even ensure that equality will |
340 | | /// behave this way when the Any contents are in the same |
341 | | /// serialization. |
342 | | /// |
343 | | /// As a result, we can only really perform a "best effort" equality |
344 | | /// test. Of course, regardless of the above, we must guarantee that |
345 | | /// hashValue is compatible with equality. |
346 | | extension AnyMessageStorage { |
347 | | // Can't use _valueData for a few reasons: |
348 | | // 1. Since decode is done on demand, two objects could be equal |
349 | | // but created differently (one from JSON, one for Message, etc.), |
350 | | // and the hash values have to be equal even if we don't have data |
351 | | // yet. |
352 | | // 2. map<> serialization order is undefined. At the time of writing |
353 | | // the Swift, Objective-C, and Go runtimes all tend to have random |
354 | | // orders, so the messages could be identical, but in binary form |
355 | | // they could differ. |
356 | 0 | public func hash(into hasher: inout Hasher) { |
357 | 0 | if !_typeURL.isEmpty { |
358 | 0 | hasher.combine(_typeURL) |
359 | 0 | } |
360 | 0 | } |
361 | | |
362 | 0 | func isEqualTo(other: AnyMessageStorage) -> Bool { |
363 | 0 | if _typeURL != other._typeURL { |
364 | 0 | return false |
365 | 0 | } |
366 | 0 |
|
367 | 0 | // Since the library does lazy Any decode, equality is a very hard problem. |
368 | 0 | // It things exactly match, that's pretty easy, otherwise, one ends up having |
369 | 0 | // to error on saying they aren't equal. |
370 | 0 | // |
371 | 0 | // The best option would be to have Message forms and compare those, as that |
372 | 0 | // removes issues like map<> serialization order, some other protocol buffer |
373 | 0 | // implementation details/bugs around serialized form order, etc.; but that |
374 | 0 | // would also greatly slow down equality tests. |
375 | 0 | // |
376 | 0 | // Do our best to compare what is present have... |
377 | 0 |
|
378 | 0 | // If both have messages, check if they are the same. |
379 | 0 | if case .message(let myMsg) = state, case .message(let otherMsg) = other.state, |
380 | 0 | type(of: myMsg) == type(of: otherMsg) |
381 | 0 | { |
382 | 0 | // Since the messages are known to be same type, we can claim both equal and |
383 | 0 | // not equal based on the equality comparison. |
384 | 0 | return myMsg.isEqualTo(message: otherMsg) |
385 | 0 | } |
386 | 0 |
|
387 | 0 | // If both have serialized data, and they exactly match; the messages are equal. |
388 | 0 | // Because there could be map in the message, the fact that the data isn't the |
389 | 0 | // same doesn't always mean the messages aren't equal. Likewise, the binary could |
390 | 0 | // have been created by a library that doesn't order the fields, or the binary was |
391 | 0 | // created using the appending ability in of the binary format. |
392 | 0 | if case .binary(let myValue) = state, case .binary(let otherValue) = other.state, myValue == otherValue { |
393 | 0 | return true |
394 | 0 | } |
395 | 0 |
|
396 | 0 | // If both have contentJSON, and they exactly match; the messages are equal. |
397 | 0 | // Because there could be map in the message (or the JSON could just be in a different |
398 | 0 | // order), the fact that the JSON isn't the same doesn't always mean the messages |
399 | 0 | // aren't equal. |
400 | 0 | if case .contentJSON(let myJSON, _) = state, |
401 | 0 | case .contentJSON(let otherJSON, _) = other.state, |
402 | 0 | myJSON == otherJSON |
403 | 0 | { |
404 | 0 | return true |
405 | 0 | } |
406 | 0 |
|
407 | 0 | // Out of options. To do more compares, the states conversions would have to be |
408 | 0 | // done to do comparisons; and since equality can be used somewhat removed from |
409 | 0 | // a developer (if they put protos in a Set, use them as keys to a Dictionary, etc), |
410 | 0 | // the conversion cost might be to high for those uses. Give up and say they aren't equal. |
411 | 0 | return false |
412 | 0 | } |
413 | | } |
414 | | |
415 | | // _CustomJSONCodable support for Google_Protobuf_Any |
416 | | extension AnyMessageStorage { |
417 | | // Spec for Any says this should contain atleast one slash. Looking at upstream languages, most |
418 | | // actually look up the value in their runtime registries, but since we do deferred parsing |
419 | | // we can't assume the registry is complete, thus just do this minimal validation check. |
420 | 4.79k | fileprivate func isTypeURLValid() -> Bool { |
421 | 108k | _typeURL.contains(where: { $0 == "/" }) |
422 | 4.79k | } |
423 | | |
424 | | // Override the traversal-based JSON encoding |
425 | | // This builds an Any JSON representation from one of: |
426 | | // * The message we were initialized with, |
427 | | // * The JSON fields we last deserialized, or |
428 | | // * The protobuf field we were deserialized from. |
429 | | // The last case requires locating the type, deserializing |
430 | | // into an object, then reserializing back to JSON. |
431 | 1.18k | func encodedJSONString(options: JSONEncodingOptions) throws -> String { |
432 | 1.18k | switch state { |
433 | 1.18k | case .binary(let valueData): |
434 | 188 | // Follow the C++ protostream_objectsource.cc's |
435 | 188 | // ProtoStreamObjectSource::RenderAny() special casing of an empty value. |
436 | 315 | if valueData.isEmpty && _typeURL.isEmpty { |
437 | 188 | return "{}" |
438 | 188 | } |
439 | 0 | guard isTypeURLValid() else { |
440 | 0 | if _typeURL.isEmpty { |
441 | 0 | throw SwiftProtobufError.JSONEncoding.emptyAnyTypeURL() |
442 | 0 | } |
443 | 0 | throw SwiftProtobufError.JSONEncoding.invalidAnyTypeURL(type_url: _typeURL) |
444 | 0 | } |
445 | 0 | if valueData.isEmpty { |
446 | 0 | var jsonEncoder = JSONEncoder() |
447 | 0 | jsonEncoder.startObject() |
448 | 0 | jsonEncoder.startField(name: "@type") |
449 | 0 | jsonEncoder.putStringValue(value: _typeURL) |
450 | 0 | jsonEncoder.endObject() |
451 | 0 | return jsonEncoder.stringResult |
452 | 0 | } |
453 | 0 | // Transcode by decoding the binary data to a message object |
454 | 0 | // and then recode back into JSON. |
455 | 0 | guard let messageType = Google_Protobuf_Any.messageType(forTypeURL: _typeURL) else { |
456 | 0 | // If we don't have the type available, we can't decode the |
457 | 0 | // binary value, so we're stuck. (The Google spec does not |
458 | 0 | // provide a way to just package the binary value for someone |
459 | 0 | // else to decode later.) |
460 | 0 | throw JSONEncodingError.anyTranscodeFailure |
461 | 0 | } |
462 | 0 | let m = try messageType.init(serializedBytes: valueData, partial: true) |
463 | 0 | return try serializeAnyJSON(for: m, typeURL: _typeURL, options: options) |
464 | 1.18k | |
465 | 1.18k | case .message(let msg): |
466 | 0 | // We should have been initialized with a typeURL, make sure it is valid. |
467 | 0 | if !_typeURL.isEmpty && !isTypeURLValid() { |
468 | 0 | throw SwiftProtobufError.JSONEncoding.invalidAnyTypeURL(type_url: _typeURL) |
469 | 0 | } |
470 | 0 | // If it was cleared, default it. |
471 | 0 | let url = !_typeURL.isEmpty ? _typeURL : buildTypeURL(forMessage: msg, typePrefix: defaultAnyTypeURLPrefix) |
472 | 0 | return try serializeAnyJSON(for: msg, typeURL: url, options: options) |
473 | 1.18k | |
474 | 1.18k | case .contentJSON(let contentJSON, _): |
475 | 1.00k | guard isTypeURLValid() else { |
476 | 0 | if _typeURL.isEmpty { |
477 | 0 | throw SwiftProtobufError.JSONEncoding.emptyAnyTypeURL() |
478 | 0 | } |
479 | 0 | throw SwiftProtobufError.JSONEncoding.invalidAnyTypeURL(type_url: _typeURL) |
480 | 1.00k | } |
481 | 1.00k | var jsonEncoder = JSONEncoder() |
482 | 1.00k | jsonEncoder.startObject() |
483 | 1.00k | jsonEncoder.startField(name: "@type") |
484 | 1.00k | jsonEncoder.putStringValue(value: _typeURL) |
485 | 1.00k | if !contentJSON.isEmpty { |
486 | 482 | jsonEncoder.append(staticText: ",") |
487 | 482 | // NOTE: This doesn't really take `options` into account since it is |
488 | 482 | // just reflecting out what was taken in originally. |
489 | 482 | jsonEncoder.append(utf8Bytes: contentJSON) |
490 | 482 | } |
491 | 1.00k | jsonEncoder.endObject() |
492 | 1.00k | return jsonEncoder.stringResult |
493 | 1.18k | } |
494 | 1.18k | } |
495 | | |
496 | | // TODO: If the type is well-known or has already been registered, |
497 | | // we should consider decoding eagerly. Eager decoding would |
498 | | // catch certain errors earlier (good) but would probably be |
499 | | // a performance hit if the Any contents were never accessed (bad). |
500 | | // Of course, we can't always decode eagerly (we don't always have the |
501 | | // message type available), so the deferred logic here is still needed. |
502 | 4.01k | func decodeJSON(from decoder: inout JSONDecoder) throws { |
503 | 4.01k | try decoder.scanner.skipRequiredObjectStart() |
504 | 4.00k | // Reset state |
505 | 4.00k | _typeURL = String() |
506 | 4.00k | state = .binary(Data()) |
507 | 4.00k | if decoder.scanner.skipOptionalObjectEnd() { |
508 | 1.03k | return |
509 | 2.97k | } |
510 | 2.97k | |
511 | 2.97k | var jsonEncoder = JSONEncoder() |
512 | 12.0k | while true { |
513 | 12.0k | let key = try decoder.scanner.nextQuotedString() |
514 | 12.0k | try decoder.scanner.skipRequiredColon() |
515 | 12.0k | if key == "@type" { |
516 | 2.67k | _typeURL = try decoder.scanner.nextQuotedString() |
517 | 2.67k | guard isTypeURLValid() else { |
518 | 18 | throw SwiftProtobufError.JSONDecoding.invalidAnyTypeURL(type_url: _typeURL) |
519 | 2.65k | } |
520 | 9.36k | } else { |
521 | 9.36k | jsonEncoder.startField(name: key) |
522 | 9.36k | let keyValueJSON = try decoder.scanner.skip() |
523 | 8.98k | jsonEncoder.append(text: keyValueJSON) |
524 | 11.6k | } |
525 | 11.6k | if decoder.scanner.skipOptionalObjectEnd() { |
526 | 2.47k | if _typeURL.isEmpty { |
527 | 11 | throw SwiftProtobufError.JSONDecoding.emptyAnyTypeURL() |
528 | 2.46k | } |
529 | 2.46k | // Capture the options, but set the messageDepthLimit to be what |
530 | 2.46k | // was left right now, as that is the limit when the JSON is finally |
531 | 2.46k | // parsed. |
532 | 2.46k | var updatedOptions = decoder.options |
533 | 2.46k | updatedOptions.messageDepthLimit = decoder.scanner.recursionBudget |
534 | 2.46k | state = .contentJSON(Array(jsonEncoder.dataResult), updatedOptions) |
535 | 2.46k | return |
536 | 9.15k | } |
537 | 9.15k | try decoder.scanner.skipRequiredComma() |
538 | 9.12k | } |
539 | 0 | } |
540 | | } |