Coverage Report

Created: 2025-09-08 06:08

/src/swift-protobuf/Sources/SwiftProtobuf/JSONEncoder.swift
Line
Count
Source (jump to first uncovered line)
1
// Sources/SwiftProtobuf/JSONEncoder.swift - JSON Encoding support
2
//
3
// Copyright (c) 2014 - 2019 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
/// JSON serialization engine.
12
///
13
// -----------------------------------------------------------------------------
14
15
import Foundation
16
17
private let asciiZero = UInt8(ascii: "0")
18
private let asciiOne = UInt8(ascii: "1")
19
private let asciiTwo = UInt8(ascii: "2")
20
private let asciiThree = UInt8(ascii: "3")
21
private let asciiFour = UInt8(ascii: "4")
22
private let asciiFive = UInt8(ascii: "5")
23
private let asciiSix = UInt8(ascii: "6")
24
private let asciiSeven = UInt8(ascii: "7")
25
private let asciiEight = UInt8(ascii: "8")
26
private let asciiNine = UInt8(ascii: "9")
27
private let asciiMinus = UInt8(ascii: "-")
28
private let asciiPlus = UInt8(ascii: "+")
29
private let asciiEquals = UInt8(ascii: "=")
30
private let asciiColon = UInt8(ascii: ":")
31
private let asciiComma = UInt8(ascii: ",")
32
private let asciiDoubleQuote = UInt8(ascii: "\"")
33
private let asciiBackslash = UInt8(ascii: "\\")
34
private let asciiForwardSlash = UInt8(ascii: "/")
35
private let asciiOpenSquareBracket = UInt8(ascii: "[")
36
private let asciiCloseSquareBracket = UInt8(ascii: "]")
37
private let asciiOpenCurlyBracket = UInt8(ascii: "{")
38
private let asciiCloseCurlyBracket = UInt8(ascii: "}")
39
private let asciiUpperA = UInt8(ascii: "A")
40
private let asciiUpperB = UInt8(ascii: "B")
41
private let asciiUpperC = UInt8(ascii: "C")
42
private let asciiUpperD = UInt8(ascii: "D")
43
private let asciiUpperE = UInt8(ascii: "E")
44
private let asciiUpperF = UInt8(ascii: "F")
45
private let asciiUpperZ = UInt8(ascii: "Z")
46
private let asciiLowerA = UInt8(ascii: "a")
47
private let asciiLowerZ = UInt8(ascii: "z")
48
49
2
private let base64Digits: [UInt8] = {
50
2
    var digits = [UInt8]()
51
2
    digits.append(contentsOf: asciiUpperA...asciiUpperZ)
52
2
    digits.append(contentsOf: asciiLowerA...asciiLowerZ)
53
2
    digits.append(contentsOf: asciiZero...asciiNine)
54
2
    digits.append(asciiPlus)
55
2
    digits.append(asciiForwardSlash)
56
2
    return digits
57
2
}()
58
59
2
private let hexDigits: [UInt8] = {
60
2
    var digits = [UInt8]()
61
2
    digits.append(contentsOf: asciiZero...asciiNine)
62
2
    digits.append(contentsOf: asciiUpperA...asciiUpperF)
63
2
    return digits
64
2
}()
65
66
internal struct JSONEncoder {
67
12.6M
    private var data = [UInt8]()
68
    private var separator: UInt8?
69
70
12.5M
    internal init() {}
71
72
606k
    internal var dataResult: [UInt8] { data }
73
74
    internal var stringResult: String {
75
4.37M
        get {
76
4.37M
            String(decoding: data, as: UTF8.self)
77
4.37M
        }
78
    }
79
80
106k
    internal var bytesResult: [UInt8] { data }
81
82
    /// Append a `StaticString` to the JSON text.  Because
83
    /// `StaticString` is already UTF8 internally, this is faster
84
    /// than appending a regular `String`.
85
8.92G
    internal mutating func append(staticText: StaticString) {
86
8.92G
        let buff = UnsafeBufferPointer(start: staticText.utf8Start, count: staticText.utf8CodeUnitCount)
87
8.92G
        data.append(contentsOf: buff)
88
8.92G
    }
89
90
    /// Append a `_NameMap.Name` to the JSON text surrounded by quotes.
91
    /// As with StaticString above, a `_NameMap.Name` provides pre-converted
92
    /// UTF8 bytes, so this is much faster than appending a regular
93
    /// `String`.
94
1.38M
    internal mutating func appendQuoted(name: _NameMap.Name) {
95
1.38M
        data.append(asciiDoubleQuote)
96
1.38M
        data.append(contentsOf: name.utf8Buffer)
97
1.38M
        data.append(asciiDoubleQuote)
98
1.38M
    }
99
100
    /// Append a `String` to the JSON text.
101
19.1M
    internal mutating func append(text: String) {
102
19.1M
        data.append(contentsOf: text.utf8)
103
19.1M
    }
104
105
    /// Append a raw utf8 in a `Data` to the JSON text.
106
0
    internal mutating func append(utf8Data: Data) {
107
0
        data.append(contentsOf: utf8Data)
108
0
    }
109
110
    /// Append a raw utf8 in a `[UInt8]` to the JSON text.
111
426k
    internal mutating func append(utf8Bytes: [UInt8]) {
112
426k
        data.append(contentsOf: utf8Bytes)
113
426k
    }
114
115
    /// Begin a new field whose name is given as a `_NameMap.Name`.
116
313k
    internal mutating func startField(name: _NameMap.Name) {
117
313k
        if let s = separator {
118
42.5k
            data.append(s)
119
313k
        }
120
313k
        appendQuoted(name: name)
121
313k
        data.append(asciiColon)
122
313k
        separator = asciiComma
123
313k
    }
124
125
    /// Begin a new field whose name is given as a `String`.
126
33.5k
    internal mutating func startField(name: String) {
127
33.5k
        if let s = separator {
128
25.5k
            data.append(s)
129
33.5k
        }
130
33.5k
        data.append(asciiDoubleQuote)
131
33.5k
        // Can avoid overhead of putStringValue, since
132
33.5k
        // the JSON field names are always clean ASCII.
133
33.5k
        data.append(contentsOf: name.utf8)
134
33.5k
        append(staticText: "\":")
135
33.5k
        separator = asciiComma
136
33.5k
    }
137
138
    /// Begin a new extension field.
139
79.6k
    internal mutating func startExtensionField(name: String) {
140
79.6k
        if let s = separator {
141
62.9k
            data.append(s)
142
79.6k
        }
143
79.6k
        append(staticText: "\"[")
144
79.6k
        data.append(contentsOf: name.utf8)
145
79.6k
        append(staticText: "]\":")
146
79.6k
        separator = asciiComma
147
79.6k
    }
148
149
    /// Append an open square bracket `[` to the JSON.
150
1.33M
    internal mutating func startArray() {
151
1.33M
        data.append(asciiOpenSquareBracket)
152
1.33M
        separator = nil
153
1.33M
    }
154
155
    /// Append a close square bracket `]` to the JSON.
156
1.33M
    internal mutating func endArray() {
157
1.33M
        data.append(asciiCloseSquareBracket)
158
1.33M
        separator = asciiComma
159
1.33M
    }
160
161
    /// Append a comma `,` to the JSON.
162
1.72M
    internal mutating func comma() {
163
1.72M
        data.append(asciiComma)
164
1.72M
    }
165
166
    /// Append an open curly brace `{` to the JSON.
167
    /// Assumes this object is part of an array of objects.
168
352k
    internal mutating func startArrayObject() {
169
352k
        if let s = separator {
170
344k
            data.append(s)
171
352k
        }
172
352k
        data.append(asciiOpenCurlyBracket)
173
352k
        separator = nil
174
352k
    }
175
176
    /// Append an open curly brace `{` to the JSON.
177
842k
    internal mutating func startObject() {
178
842k
        data.append(asciiOpenCurlyBracket)
179
842k
        separator = nil
180
842k
    }
181
182
    /// Append a close curly brace `}` to the JSON.
183
11.1M
    internal mutating func endObject() {
184
11.1M
        data.append(asciiCloseCurlyBracket)
185
11.1M
        separator = asciiComma
186
11.1M
    }
187
188
    /// Write a JSON `null` token to the output.
189
201
    internal mutating func putNullValue() {
190
201
        append(staticText: "null")
191
201
    }
192
193
    /// Append a float value to the output.
194
    /// This handles Nan and infinite values by
195
    /// writing well-known string values.
196
673k
    internal mutating func putFloatValue(value: Float) {
197
673k
        if value.isNaN {
198
9.36k
            append(staticText: "\"NaN\"")
199
673k
        } else if !value.isFinite {
200
52.6k
            if value < 0 {
201
32.4k
                append(staticText: "\"-Infinity\"")
202
52.6k
            } else {
203
20.2k
                append(staticText: "\"Infinity\"")
204
52.6k
            }
205
673k
        } else {
206
620k
            data.append(contentsOf: value.debugDescription.utf8)
207
673k
        }
208
673k
    }
209
210
    /// Append a double value to the output.
211
    /// This handles Nan and infinite values by
212
    /// writing well-known string values.
213
1.08M
    internal mutating func putDoubleValue(value: Double) {
214
1.08M
        if value.isNaN {
215
8.52k
            append(staticText: "\"NaN\"")
216
1.08M
        } else if !value.isFinite {
217
50.2k
            if value < 0 {
218
34.0k
                append(staticText: "\"-Infinity\"")
219
50.2k
            } else {
220
16.2k
                append(staticText: "\"Infinity\"")
221
50.2k
            }
222
1.08M
        } else {
223
1.03M
            data.append(contentsOf: value.debugDescription.utf8)
224
1.08M
        }
225
1.08M
    }
226
227
    /// Append a UInt64 to the output (without quoting).
228
1.42M
    private mutating func appendUInt(value: UInt64) {
229
1.42M
        if value >= 10 {
230
407k
            appendUInt(value: value / 10)
231
1.42M
        }
232
1.42M
        data.append(asciiZero + UInt8(value % 10))
233
1.42M
    }
234
235
    /// Append an Int64 to the output (without quoting).
236
4.20M
    private mutating func appendInt(value: Int64) {
237
4.20M
        if value < 0 {
238
562k
            data.append(asciiMinus)
239
562k
            // This is the twos-complement negation of value,
240
562k
            // computed in a way that won't overflow a 64-bit
241
562k
            // signed integer.
242
562k
            appendUInt(value: 1 + ~UInt64(bitPattern: value))
243
4.20M
        } else {
244
3.63M
            appendUInt(value: UInt64(bitPattern: value))
245
4.20M
        }
246
4.20M
    }
247
248
    /// Write an Enum as an Int.
249
0
    internal mutating func putEnumInt(value: Int) {
250
0
        appendInt(value: Int64(value))
251
0
    }
252
253
    /// Write an `Int64` using protobuf JSON quoting conventions.
254
3.50M
    internal mutating func putQuotedInt64(value: Int64) {
255
3.50M
        data.append(asciiDoubleQuote)
256
3.50M
        appendInt(value: value)
257
3.50M
        data.append(asciiDoubleQuote)
258
3.50M
    }
259
260
0
    internal mutating func putNonQuotedInt64(value: Int64) {
261
0
        appendInt(value: value)
262
0
    }
263
264
    /// Write an `Int32` with quoting suitable for
265
    /// using the value as a map key.
266
25.4k
    internal mutating func putQuotedInt32(value: Int32) {
267
25.4k
        data.append(asciiDoubleQuote)
268
25.4k
        appendInt(value: Int64(value))
269
25.4k
        data.append(asciiDoubleQuote)
270
25.4k
    }
271
272
    /// Write an `Int32` in the default format.
273
1.89M
    internal mutating func putNonQuotedInt32(value: Int32) {
274
1.89M
        appendInt(value: Int64(value))
275
1.89M
    }
276
277
    /// Write a `UInt64` using protobuf JSON quoting conventions.
278
4.15M
    internal mutating func putQuotedUInt64(value: UInt64) {
279
4.15M
        data.append(asciiDoubleQuote)
280
4.15M
        appendUInt(value: value)
281
4.15M
        data.append(asciiDoubleQuote)
282
4.15M
    }
283
284
0
    internal mutating func putNonQuotedUInt64(value: UInt64) {
285
0
        appendUInt(value: value)
286
0
    }
287
288
    /// Write a `UInt32` with quoting suitable for
289
    /// using the value as a map key.
290
100k
    internal mutating func putQuotedUInt32(value: UInt32) {
291
100k
        data.append(asciiDoubleQuote)
292
100k
        appendUInt(value: UInt64(value))
293
100k
        data.append(asciiDoubleQuote)
294
100k
    }
295
296
    /// Write a `UInt32` in the default format.
297
315k
    internal mutating func putNonQuotedUInt32(value: UInt32) {
298
315k
        appendUInt(value: UInt64(value))
299
315k
    }
300
301
    /// Write a `Bool` with quoting suitable for
302
    /// using the value as a map key.
303
2.92k
    internal mutating func putQuotedBoolValue(value: Bool) {
304
2.92k
        data.append(asciiDoubleQuote)
305
2.92k
        putNonQuotedBoolValue(value: value)
306
2.92k
        data.append(asciiDoubleQuote)
307
2.92k
    }
308
309
    /// Write a `Bool` in the default format.
310
499k
    internal mutating func putNonQuotedBoolValue(value: Bool) {
311
499k
        if value {
312
254k
            append(staticText: "true")
313
499k
        } else {
314
245k
            append(staticText: "false")
315
499k
        }
316
499k
    }
317
318
    /// Append a string value escaping special characters as needed.
319
173k
    internal mutating func putStringValue(value: String) {
320
173k
        data.append(asciiDoubleQuote)
321
142M
        for c in value.unicodeScalars {
322
142M
            switch c.value {
323
142M
            // Special two-byte escapes
324
142M
            case 8: append(staticText: "\\b")
325
142M
            case 9: append(staticText: "\\t")
326
142M
            case 10: append(staticText: "\\n")
327
142M
            case 12: append(staticText: "\\f")
328
142M
            case 13: append(staticText: "\\r")
329
142M
            case 34: append(staticText: "\\\"")
330
142M
            case 92: append(staticText: "\\\\")
331
142M
            case 0...31, 127...159:  // Hex form for C0 control chars
332
138M
                append(staticText: "\\u00")
333
138M
                data.append(hexDigits[Int(c.value / 16)])
334
138M
                data.append(hexDigits[Int(c.value & 15)])
335
142M
            case 23...126:
336
3.52M
                data.append(UInt8(truncatingIfNeeded: c.value))
337
142M
            case 0x80...0x7ff:
338
20.9k
                data.append(0xc0 + UInt8(truncatingIfNeeded: c.value >> 6))
339
20.9k
                data.append(0x80 + UInt8(truncatingIfNeeded: c.value & 0x3f))
340
142M
            case 0x800...0xffff:
341
14.4k
                data.append(0xe0 + UInt8(truncatingIfNeeded: c.value >> 12))
342
14.4k
                data.append(0x80 + UInt8(truncatingIfNeeded: (c.value >> 6) & 0x3f))
343
14.4k
                data.append(0x80 + UInt8(truncatingIfNeeded: c.value & 0x3f))
344
142M
            default:
345
20.5k
                data.append(0xf0 + UInt8(truncatingIfNeeded: c.value >> 18))
346
20.5k
                data.append(0x80 + UInt8(truncatingIfNeeded: (c.value >> 12) & 0x3f))
347
20.5k
                data.append(0x80 + UInt8(truncatingIfNeeded: (c.value >> 6) & 0x3f))
348
20.5k
                data.append(0x80 + UInt8(truncatingIfNeeded: c.value & 0x3f))
349
142M
            }
350
142M
        }
351
173k
        data.append(asciiDoubleQuote)
352
173k
    }
353
354
    /// Append a bytes value using protobuf JSON Base-64 encoding.
355
13.6k
    internal mutating func putBytesValue<Bytes: SwiftProtobufContiguousBytes>(value: Bytes) {
356
13.6k
        data.append(asciiDoubleQuote)
357
13.6k
        if value.count > 0 {
358
10.7k
            value.withUnsafeBytes { (body: UnsafeRawBufferPointer) in
359
10.7k
                if let p = body.baseAddress, body.count > 0 {
360
10.7k
                    var t: Int = 0
361
10.7k
                    var bytesInGroup: Int = 0
362
153k
                    for i in 0..<body.count {
363
153k
                        if bytesInGroup == 3 {
364
44.4k
                            data.append(base64Digits[(t >> 18) & 63])
365
44.4k
                            data.append(base64Digits[(t >> 12) & 63])
366
44.4k
                            data.append(base64Digits[(t >> 6) & 63])
367
44.4k
                            data.append(base64Digits[t & 63])
368
44.4k
                            t = 0
369
44.4k
                            bytesInGroup = 0
370
153k
                        }
371
153k
                        t = (t << 8) + Int(p[i])
372
153k
                        bytesInGroup += 1
373
153k
                    }
374
10.7k
                    switch bytesInGroup {
375
10.7k
                    case 3:
376
3.08k
                        data.append(base64Digits[(t >> 18) & 63])
377
3.08k
                        data.append(base64Digits[(t >> 12) & 63])
378
3.08k
                        data.append(base64Digits[(t >> 6) & 63])
379
3.08k
                        data.append(base64Digits[t & 63])
380
10.7k
                    case 2:
381
3.00k
                        t <<= 8
382
3.00k
                        data.append(base64Digits[(t >> 18) & 63])
383
3.00k
                        data.append(base64Digits[(t >> 12) & 63])
384
3.00k
                        data.append(base64Digits[(t >> 6) & 63])
385
3.00k
                        data.append(asciiEquals)
386
10.7k
                    case 1:
387
4.69k
                        t <<= 16
388
4.69k
                        data.append(base64Digits[(t >> 18) & 63])
389
4.69k
                        data.append(base64Digits[(t >> 12) & 63])
390
4.69k
                        data.append(asciiEquals)
391
4.69k
                        data.append(asciiEquals)
392
10.7k
                    default:
393
0
                        break
394
10.7k
                    }
395
10.7k
                }
396
10.7k
            }
397
13.6k
        }
398
13.6k
        data.append(asciiDoubleQuote)
399
13.6k
    }
400
}