Coverage Report

Created: 2026-05-16 06:46

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}