Coverage Report

Created: 2025-09-05 06:37

/src/swift-protobuf/Sources/SwiftProtobuf/Google_Protobuf_FieldMask+Extensions.swift
Line
Count
Source (jump to first uncovered line)
1
// Sources/SwiftProtobuf/Google_Protobuf_FieldMask+Extensions.swift - Fieldmask extensions
2
//
3
// Copyright (c) 2014 - 2016 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
/// Extend the generated FieldMask message with customized JSON coding and
12
/// convenience methods.
13
///
14
// -----------------------------------------------------------------------------
15
16
// True if the string only contains printable (non-control)
17
// ASCII characters.  Note: This follows the ASCII standard;
18
// space is not a "printable" character.
19
1.86M
private func isPrintableASCII(_ s: String) -> Bool {
20
1.90M
    for u in s.utf8 {
21
1.90M
        if u <= 0x20 || u >= 0x7f {
22
122
            return false
23
1.90M
        }
24
1.90M
    }
25
1.86M
    return true
26
1.86M
}
27
28
798k
private func ProtoToJSON(name: String) -> String? {
29
798k
    guard isPrintableASCII(name) else { return nil }
30
798k
    var jsonPath = String()
31
798k
    var chars = name.makeIterator()
32
816k
    while let c = chars.next() {
33
816k
        switch c {
34
816k
        case "_":
35
2.06k
            if let toupper = chars.next() {
36
2.06k
                switch toupper {
37
2.06k
                case "a"..."z":
38
2.06k
                    jsonPath.append(String(toupper).uppercased())
39
2.06k
                default:
40
0
                    return nil
41
2.06k
                }
42
2.06k
            } else {
43
0
                return nil
44
816k
            }
45
816k
        case "A"..."Z":
46
0
            return nil
47
816k
        case "a"..."z", "0"..."9", ".", "(", ")":
48
813k
            jsonPath.append(c)
49
816k
        default:
50
1.49k
            // TODO: Change this to `return nil`
51
1.49k
            // once we know everything legal is handled
52
1.49k
            // above.
53
1.49k
            jsonPath.append(c)
54
816k
        }
55
816k
    }
56
798k
    return jsonPath
57
798k
}
58
59
1.06M
private func JSONToProto(name: String) -> String? {
60
1.06M
    guard isPrintableASCII(name) else { return nil }
61
1.06M
    var path = String()
62
1.08M
    for c in name {
63
1.08M
        switch c {
64
1.08M
        case "_":
65
3
            return nil
66
1.08M
        case "A"..."Z":
67
3.25k
            path.append(Character("_"))
68
3.25k
            path.append(String(c).lowercased())
69
1.08M
        case "a"..."z", "0"..."9", ".", "(", ")":
70
1.08M
            path.append(c)
71
1.08M
        default:
72
2.64k
            // TODO: Change to `return nil` once
73
2.64k
            // we know everything legal is being
74
2.64k
            // handled above
75
2.64k
            path.append(c)
76
1.08M
        }
77
1.08M
    }
78
1.06M
    return path
79
1.06M
}
80
81
4.56k
private func parseJSONFieldNames(names: String) -> [String]? {
82
4.56k
    // An empty field mask is the empty string (no paths).
83
4.56k
    guard !names.isEmpty else { return [] }
84
2.79k
    var fieldNameCount = 0
85
2.79k
    var fieldName = String()
86
2.79k
    var split = [String]()
87
2.16M
    for c in names {
88
2.16M
        switch c {
89
2.16M
        case ",":
90
1.06M
            if fieldNameCount == 0 {
91
11
                return nil
92
1.06M
            }
93
1.06M
            if let pbName = JSONToProto(name: fieldName) {
94
1.06M
                split.append(pbName)
95
1.06M
            } else {
96
8
                return nil
97
1.06M
            }
98
1.06M
            fieldName = String()
99
1.06M
            fieldNameCount = 0
100
2.16M
        default:
101
1.09M
            fieldName.append(c)
102
1.09M
            fieldNameCount += 1
103
2.16M
        }
104
2.16M
    }
105
2.77k
    if fieldNameCount == 0 {  // Last field name can't be empty
106
68
        return nil
107
2.71k
    }
108
2.71k
    if let pbName = JSONToProto(name: fieldName) {
109
2.59k
        split.append(pbName)
110
2.71k
    } else {
111
117
        return nil
112
2.59k
    }
113
2.59k
    return split
114
2.71k
}
115
116
extension Google_Protobuf_FieldMask {
117
    /// Creates a new `Google_Protobuf_FieldMask` from the given array of paths.
118
    ///
119
    /// The paths should match the names used in the .proto file, which may be
120
    /// different than the corresponding Swift property names.
121
    ///
122
    /// - Parameter protoPaths: The paths from which to create the field mask,
123
    ///   defined using the .proto names for the fields.
124
0
    public init(protoPaths: [String]) {
125
0
        self.init()
126
0
        paths = protoPaths
127
0
    }
128
129
    /// Creates a new `Google_Protobuf_FieldMask` from the given paths.
130
    ///
131
    /// The paths should match the names used in the .proto file, which may be
132
    /// different than the corresponding Swift property names.
133
    ///
134
    /// - Parameter protoPaths: The paths from which to create the field mask,
135
    ///   defined using the .proto names for the fields.
136
0
    public init(protoPaths: String...) {
137
0
        self.init(protoPaths: protoPaths)
138
0
    }
139
140
    /// Creates a new `Google_Protobuf_FieldMask` from the given paths.
141
    ///
142
    /// The paths should match the JSON names of the fields, which may be
143
    /// different than the corresponding Swift property names.
144
    ///
145
    /// - Parameter jsonPaths: The paths from which to create the field mask,
146
    ///   defined using the JSON names for the fields.
147
0
    public init?(jsonPaths: String...) {
148
0
        // TODO: This should fail if any of the conversions from JSON fails
149
0
        self.init(protoPaths: jsonPaths.compactMap(JSONToProto))
150
0
    }
151
152
    // It would be nice if to have an initializer that accepted Swift property
153
    // names, but translating between swift and protobuf/json property
154
    // names is not entirely deterministic.
155
}
156
157
extension Google_Protobuf_FieldMask: _CustomJSONCodable {
158
2.70k
    mutating func decodeJSON(from decoder: inout JSONDecoder) throws {
159
2.70k
        let s = try decoder.scanner.nextQuotedString()
160
2.70k
        if let names = parseJSONFieldNames(names: s) {
161
2.56k
            paths = names
162
2.70k
        } else {
163
146
            throw JSONDecodingError.malformedFieldMask
164
2.56k
        }
165
2.56k
    }
166
167
1.14k
    func encodedJSONString(options: JSONEncodingOptions) throws -> String {
168
1.14k
        // Note:  Proto requires alphanumeric field names, so there
169
1.14k
        // cannot be a ',' or '"' character to mess up this formatting.
170
1.14k
        var jsonPaths = [String]()
171
797k
        for p in paths {
172
797k
            if let jsonPath = ProtoToJSON(name: p) {
173
797k
                jsonPaths.append(jsonPath)
174
797k
            } else {
175
0
                throw JSONEncodingError.fieldMaskConversion
176
797k
            }
177
797k
        }
178
1.14k
        return "\"" + jsonPaths.joined(separator: ",") + "\""
179
1.14k
    }
180
}
181
182
extension Google_Protobuf_FieldMask {
183
184
    /// Initiates a field mask with all fields of the message type.
185
    ///
186
    /// - Parameter messageType: Message type to get all paths from.
187
    public init<M: Message & _ProtoNameProviding>(
188
        allFieldsOf messageType: M.Type
189
0
    ) {
190
0
        self = .with { mask in
191
0
            mask.paths = M.allProtoNames
192
0
        }
193
0
    }
194
195
    /// Initiates a field mask from some particular field numbers of a message
196
    ///
197
    /// - Parameters:
198
    ///   - messageType: Message type to get all paths from.
199
    ///   - fieldNumbers: Field numbers of paths to be included.
200
    /// - Returns: Field mask that include paths of corresponding field numbers.
201
    /// - Throws: `FieldMaskError.invalidFieldNumber` if the field number
202
    ///  is not on the message
203
    public init<M: Message & _ProtoNameProviding>(
204
        fieldNumbers: [Int],
205
        of messageType: M.Type
206
0
    ) throws {
207
0
        var paths: [String] = []
208
0
        for number in fieldNumbers {
209
0
            guard let name = M.protoName(for: number) else {
210
0
                throw FieldMaskError.invalidFieldNumber
211
0
            }
212
0
            paths.append(name)
213
0
        }
214
0
        self = .with { mask in
215
0
            mask.paths = paths
216
0
        }
217
0
    }
218
}
219
220
extension Google_Protobuf_FieldMask {
221
222
    /// Adds a path to FieldMask after checking whether the given path is valid.
223
    /// This method check-fails if the path is not a valid path for Message type.
224
    ///
225
    /// - Parameters:
226
    ///   - path: Path to be added to FieldMask.
227
    ///   - messageType: Message type to check validity.
228
    public mutating func addPath<M: Message>(
229
        _ path: String,
230
        of messageType: M.Type
231
0
    ) throws {
232
0
        guard M.isPathValid(path) else {
233
0
            throw FieldMaskError.invalidPath
234
0
        }
235
0
        paths.append(path)
236
0
    }
237
238
    /// Converts a FieldMask to the canonical form. It will:
239
    ///   1. Remove paths that are covered by another path. For example,
240
    ///      "foo.bar" is covered by "foo" and will be removed if "foo"
241
    ///      is also in the FieldMask.
242
    ///   2. Sort all paths in alphabetical order.
243
0
    public var canonical: Google_Protobuf_FieldMask {
244
0
        var mask = Google_Protobuf_FieldMask()
245
0
        let sortedPaths = self.paths.sorted()
246
0
        for path in sortedPaths {
247
0
            if let lastPath = mask.paths.last {
248
0
                if path != lastPath, !path.hasPrefix("\(lastPath).") {
249
0
                    mask.paths.append(path)
250
0
                }
251
0
            } else {
252
0
                mask.paths.append(path)
253
0
            }
254
0
        }
255
0
        return mask
256
0
    }
257
258
    /// Creates an union of two FieldMasks.
259
    ///
260
    /// - Parameter mask: FieldMask to union with.
261
    /// - Returns: FieldMask with union of two path sets.
262
    public func union(
263
        _ mask: Google_Protobuf_FieldMask
264
0
    ) -> Google_Protobuf_FieldMask {
265
0
        var buffer: Set<String> = .init()
266
0
        var paths: [String] = []
267
0
        let allPaths = self.paths + mask.paths
268
0
        for path in allPaths where !buffer.contains(path) {
269
0
            buffer.insert(path)
270
0
            paths.append(path)
271
0
        }
272
0
        return .with { mask in
273
0
            mask.paths = paths
274
0
        }
275
0
    }
276
277
    /// Creates an intersection of two FieldMasks.
278
    ///
279
    /// - Parameter mask: FieldMask to intersect with.
280
    /// - Returns: FieldMask with intersection of two path sets.
281
    public func intersect(
282
        _ mask: Google_Protobuf_FieldMask
283
0
    ) -> Google_Protobuf_FieldMask {
284
0
        let set = Set<String>(mask.paths)
285
0
        var paths: [String] = []
286
0
        var buffer = Set<String>()
287
0
        for path in self.paths where set.contains(path) && !buffer.contains(path) {
288
0
            buffer.insert(path)
289
0
            paths.append(path)
290
0
        }
291
0
        return .with { mask in
292
0
            mask.paths = paths
293
0
        }
294
0
    }
295
296
    /// Creates a FieldMasks with paths of the original FieldMask
297
    /// that does not included in mask.
298
    ///
299
    /// - Parameter mask: FieldMask with paths should be substracted.
300
    /// - Returns: FieldMask with all paths does not included in mask.
301
    public func subtract(
302
        _ mask: Google_Protobuf_FieldMask
303
0
    ) -> Google_Protobuf_FieldMask {
304
0
        let set = Set<String>(mask.paths)
305
0
        var paths: [String] = []
306
0
        var buffer = Set<String>()
307
0
        for path in self.paths where !set.contains(path) && !buffer.contains(path) {
308
0
            buffer.insert(path)
309
0
            paths.append(path)
310
0
        }
311
0
        return .with { mask in
312
0
            mask.paths = paths
313
0
        }
314
0
    }
315
316
    /// Returns true if path is covered by the given FieldMask. Note that path
317
    /// "foo.bar" covers all paths like "foo.bar.baz", "foo.bar.quz.x", etc.
318
    /// Also note that parent paths are not covered by explicit child path, i.e.
319
    /// "foo.bar" does NOT cover "foo", even if "bar" is the only child.
320
    ///
321
    /// - Parameter path: Path to be checked.
322
    /// - Returns: Boolean determines is path covered.
323
0
    public func contains(_ path: String) -> Bool {
324
0
        for fieldMaskPath in paths {
325
0
            if path.hasPrefix("\(fieldMaskPath).") || fieldMaskPath == path {
326
0
                return true
327
0
            }
328
0
        }
329
0
        return false
330
0
    }
331
}
332
333
extension Google_Protobuf_FieldMask {
334
335
    /// Checks whether the given FieldMask is valid for type M.
336
    ///
337
    /// - Parameter messageType: Message type to paths check with.
338
    /// - Returns: Boolean determines FieldMask is valid.
339
    public func isValid<M: Message & _ProtoNameProviding>(
340
        for messageType: M.Type
341
0
    ) -> Bool {
342
0
        var message = M()
343
0
        return paths.allSatisfy { path in
344
0
            message.isPathValid(path)
345
0
        }
346
0
    }
347
}
348
349
/// Describes errors could happen during FieldMask utilities.
350
public enum FieldMaskError: Error {
351
352
    /// Describes a path is invalid for a Message type.
353
    case invalidPath
354
355
    /// Describes a fieldNumber is invalid for a Message type.
356
    case invalidFieldNumber
357
}
358
359
extension Message where Self: _ProtoNameProviding {
360
0
    fileprivate static func protoName(for number: Int) -> String? {
361
0
        Self._protobuf_nameMap.names(for: number)?.proto.description
362
0
    }
363
364
0
    fileprivate static var allProtoNames: [String] {
365
0
        Self._protobuf_nameMap.names.map(\.description)
366
0
    }
367
}