Coverage Report

Created: 2026-06-30 06:42

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/swift-protobuf/Sources/SwiftProtobuf/Google_Protobuf_Duration+Extensions.swift
Line
Count
Source
1
// Sources/SwiftProtobuf/Google_Protobuf_Duration+Extensions.swift - Extensions for Duration type
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
/// Extends the generated Duration struct with various custom behaviors:
12
/// * JSON coding and decoding
13
/// * Arithmetic operations
14
///
15
// -----------------------------------------------------------------------------
16
17
#if canImport(FoundationEssentials)
18
import FoundationEssentials
19
#else
20
import Foundation
21
#endif
22
23
private let minDurationSeconds: Int64 = -maxDurationSeconds
24
private let maxDurationSeconds: Int64 = 315_576_000_000
25
private let minDurationNanos: Int32 = -maxDurationNanos
26
private let maxDurationNanos: Int32 = 999_999_999
27
28
4.95k
private func parseDuration(text: String) throws -> (Int64, Int32) {
29
4.95k
    var digits = [Character]()
30
4.95k
    var digitCount = 0
31
4.95k
    var total = 0
32
4.95k
    var chars = text.makeIterator()
33
4.95k
    var seconds: Int64?
34
4.95k
    var nanos: Int32 = 0
35
4.95k
    var isNegative = false
36
3.41M
    while let c = chars.next() {
37
3.41M
        switch c {
38
3.41M
        case "-":
39
2.09k
            // Only accept '-' as very first character
40
2.09k
            if total > 0 {
41
7
                throw JSONDecodingError.malformedDuration
42
2.08k
            }
43
2.08k
            digits.append(c)
44
2.08k
            isNegative = true
45
3.41M
        case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9":
46
3.40M
            digits.append(c)
47
3.40M
            digitCount += 1
48
3.41M
        case ".":
49
3.25k
            if let _ = seconds {
50
2
                throw JSONDecodingError.malformedDuration
51
3.24k
            }
52
3.24k
            let digitString = String(digits)
53
3.24k
            if let s = Int64(digitString),
54
3.24k
                s >= minDurationSeconds && s <= maxDurationSeconds
55
3.24k
            {
56
3.16k
                seconds = s
57
3.16k
            } else {
58
86
                throw JSONDecodingError.malformedDuration
59
3.16k
            }
60
3.16k
            digits.removeAll()
61
3.16k
            digitCount = 0
62
3.41M
        case "s":
63
4.51k
            if let _ = seconds {
64
3.10k
                // Seconds already set, digits holds nanos. The fraction must
65
3.10k
                // have between 1 and 9 digits; matches the protobuf grammar
66
3.10k
                // and the reference JSON parser, which reject an empty
67
3.10k
                // fraction or more than nanosecond precision.
68
3.10k
                if digitCount < 1 || digitCount > 9 {
69
31
                    throw JSONDecodingError.malformedDuration
70
3.07k
                }
71
19.8k
                while digitCount < 9 {
72
16.7k
                    digits.append(Character("0"))
73
16.7k
                    digitCount += 1
74
16.7k
                }
75
3.07k
                let digitString = String(digits)
76
3.07k
                if let rawNanos = Int32(digitString) {
77
3.07k
                    if isNegative {
78
1.31k
                        nanos = -rawNanos
79
1.75k
                    } else {
80
1.75k
                        nanos = rawNanos
81
1.75k
                    }
82
3.07k
                } else {
83
0
                    throw JSONDecodingError.malformedDuration
84
3.07k
                }
85
3.07k
            } else {
86
1.41k
                // No fraction, we just have an integral number of seconds
87
1.41k
                let digitString = String(digits)
88
1.41k
                if let s = Int64(digitString),
89
1.41k
                    s >= minDurationSeconds && s <= maxDurationSeconds
90
1.41k
                {
91
1.31k
                    seconds = s
92
1.31k
                } else {
93
99
                    throw JSONDecodingError.malformedDuration
94
1.31k
                }
95
4.38k
            }
96
4.38k
            // Fail if there are characters after 's'
97
4.38k
            if chars.next() != nil {
98
32
                throw JSONDecodingError.malformedDuration
99
4.35k
            }
100
4.35k
            return (seconds!, nanos)
101
3.41M
        default:
102
156
            throw JSONDecodingError.malformedDuration
103
3.41M
        }
104
3.40M
        total += 1
105
3.40M
    }
106
185
    throw JSONDecodingError.malformedDuration
107
4.95k
}
108
109
2.36k
private func formatDuration(seconds: Int64, nanos: Int32) -> String? {
110
2.36k
    // Upstream's json file unparse.cc:WriteDuration() for these checks for reference.
111
2.36k
112
2.36k
    // Range check...
113
2.36k
    guard
114
2.36k
        (seconds >= minDurationSeconds && seconds <= maxDurationSeconds)
115
2.36k
            && (nanos >= minDurationNanos && nanos <= maxDurationNanos)
116
2.36k
    else {
117
0
        return nil
118
2.36k
    }
119
2.36k
    // Either seconds or nanos has to be zero otherwise the signs must match.
120
2.36k
    guard (seconds == 0) || (nanos == 0) || ((seconds < 0) == (nanos < 0)) else {
121
0
        return nil
122
2.36k
    }
123
2.36k
    let nanosString = nanosToString(nanos: nanos)  // Includes leading '.' if needed
124
2.36k
    if seconds == 0 && nanos < 0 {
125
391
        return "-0\(nanosString)s"
126
1.97k
    }
127
1.97k
    return "\(seconds)\(nanosString)s"
128
2.36k
}
129
130
extension Google_Protobuf_Duration {
131
    /// Creates a new `Google_Protobuf_Duration` equal to the given number of
132
    /// seconds and nanoseconds.
133
    ///
134
    /// - Parameter seconds: The number of seconds.
135
    /// - Parameter nanos: The number of nanoseconds.
136
0
    public init(seconds: Int64 = 0, nanos: Int32 = 0) {
137
0
        self.init()
138
0
        self.seconds = seconds
139
0
        self.nanos = nanos
140
0
    }
141
}
142
143
extension Google_Protobuf_Duration: _CustomJSONCodable {
144
3.50k
    mutating func decodeJSON(from decoder: inout JSONDecoder) throws {
145
3.50k
        let s = try decoder.scanner.nextQuotedString()
146
3.48k
        (seconds, nanos) = try parseDuration(text: s)
147
3.07k
    }
148
1.53k
    func encodedJSONString(options: JSONEncodingOptions) throws -> String {
149
1.53k
        if let formatted = formatDuration(seconds: seconds, nanos: nanos) {
150
1.53k
            return "\"\(formatted)\""
151
1.53k
        } else {
152
0
            throw JSONEncodingError.durationRange
153
0
        }
154
1.53k
    }
155
}
156
157
extension Google_Protobuf_Duration: ExpressibleByFloatLiteral {
158
    public typealias FloatLiteralType = Double
159
160
    /// Creates a new `Google_Protobuf_Duration` from a floating point literal
161
    /// that is interpreted as a duration in seconds, rounded to the nearest
162
    /// nanosecond.
163
0
    public init(floatLiteral value: Double) {
164
0
        self.init(rounding: value, rule: .toNearestOrAwayFromZero)
165
0
    }
166
}
167
168
extension Google_Protobuf_Duration {
169
    #if !REMOVE_DEPRECATED_APIS
170
    /// Creates a new `Google_Protobuf_Duration` that is equal to the given
171
    /// `TimeInterval` (measured in seconds), rounded to the nearest nanosecond.
172
    ///
173
    /// - Parameter timeInterval: The `TimeInterval`.
174
    @available(*, deprecated, renamed: "init(rounding:rule:)")
175
0
    public init(timeInterval: TimeInterval) {
176
0
        self.init(rounding: timeInterval, rule: .toNearestOrAwayFromZero)
177
0
    }
178
    #endif  // !REMOVE_DEPRECATED_APIS
179
180
    /// Creates a new `Google_Protobuf_Duration` that is equal to the given
181
    /// `TimeInterval` (measured in seconds), rounded to the nearest nanosecond
182
    /// according to the given rounding rule.
183
    ///
184
    /// - Parameters:
185
    ///   - timeInterval: The `TimeInterval`.
186
    ///   - rule: The rounding rule to use.
187
    public init(
188
        rounding timeInterval: TimeInterval,
189
        rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero
190
0
    ) {
191
0
        let sd = Int64(timeInterval)
192
0
        let nd = ((timeInterval - Double(sd)) * TimeInterval(nanosPerSecond)).rounded(rule)
193
0
        // Normalize is here incase things round up to a full second worth of nanos.
194
0
        let (s, n) = normalizeForDuration(seconds: sd, nanos: Int32(nd))
195
0
        self.init(seconds: s, nanos: n)
196
0
    }
197
198
    /// The `TimeInterval` (measured in seconds) equal to this duration.
199
0
    public var timeInterval: TimeInterval {
200
0
        TimeInterval(self.seconds) + TimeInterval(self.nanos) / TimeInterval(nanosPerSecond)
201
0
    }
202
}
203
204
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
205
extension Google_Protobuf_Duration {
206
    /// Creates a new `Google_Protobuf_Duration` by rounding a `Duration` to
207
    /// the nearest nanosecond according to the given rounding rule.
208
    ///
209
    /// - Parameters:
210
    ///   - duration: The `Duration`.
211
    ///   - rule: The rounding rule to use.
212
    public init(
213
        rounding duration: Duration,
214
        rule: FloatingPointRoundingRule = .toNearestOrAwayFromZero
215
0
    ) {
216
0
        let secs = duration.components.seconds
217
0
        let attos = duration.components.attoseconds
218
0
        let fracNanos =
219
0
            (Double(attos % attosPerNanosecond) / Double(attosPerNanosecond)).rounded(rule)
220
0
        let nanos = Int32(attos / attosPerNanosecond) + Int32(fracNanos)
221
0
        // Normalize is here incase things round up to a full second worth of nanos.
222
0
        let (s, n) = normalizeForDuration(seconds: secs, nanos: nanos)
223
0
        self.init(seconds: s, nanos: n)
224
0
    }
225
}
226
227
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
228
extension Duration {
229
    /// Creates a new `Duration` that is equal to the given duration.
230
    ///
231
    /// Swift `Duration` has a strictly higher precision than `Google_Protobuf_Duration`
232
    /// (attoseconds vs. nanoseconds, respectively), so this conversion is always
233
    /// value-preserving.
234
    ///
235
    /// - Parameters:
236
    ///   - duration: The `Google_Protobuf_Duration`.
237
0
    public init(_ duration: Google_Protobuf_Duration) {
238
0
        self.init(
239
0
            secondsComponent: duration.seconds,
240
0
            attosecondsComponent: Int64(duration.nanos) * attosPerNanosecond
241
0
        )
242
0
    }
243
}
244
245
private func normalizeForDuration(
246
    seconds: Int64,
247
    nanos: Int32
248
0
) -> (seconds: Int64, nanos: Int32) {
249
0
    var s = seconds
250
0
    var n = nanos
251
0
252
0
    // If the magnitude of n exceeds a second then
253
0
    // we need to factor it into s instead.
254
0
    if n >= nanosPerSecond || n <= -nanosPerSecond {
255
0
        s += Int64(n / nanosPerSecond)
256
0
        n = n % nanosPerSecond
257
0
    }
258
0
259
0
    // The Duration spec says that when s != 0, s and
260
0
    // n must have the same sign.
261
0
    if s > 0 && n < 0 {
262
0
        n += nanosPerSecond
263
0
        s -= 1
264
0
    } else if s < 0 && n > 0 {
265
0
        n -= nanosPerSecond
266
0
        s += 1
267
0
    }
268
0
269
0
    return (seconds: s, nanos: n)
270
0
}
271
272
public prefix func - (
273
    operand: Google_Protobuf_Duration
274
0
) -> Google_Protobuf_Duration {
275
0
    // This gets normalized (thus allowing an otherwise non-logical input) so it matches what would
276
0
    // happen if doing `Duration(0,0) - operand` because that has to normalize to handle roll
277
0
    // over/under.
278
0
    let (s, n) = normalizeForDuration(
279
0
        seconds: -operand.seconds,
280
0
        nanos: -operand.nanos
281
0
    )
282
0
    return Google_Protobuf_Duration(seconds: s, nanos: n)
283
0
}
284
285
public func + (
286
    lhs: Google_Protobuf_Duration,
287
    rhs: Google_Protobuf_Duration
288
0
) -> Google_Protobuf_Duration {
289
0
    let (s, n) = normalizeForDuration(
290
0
        seconds: lhs.seconds + rhs.seconds,
291
0
        nanos: lhs.nanos + rhs.nanos
292
0
    )
293
0
    return Google_Protobuf_Duration(seconds: s, nanos: n)
294
0
}
295
296
public func - (
297
    lhs: Google_Protobuf_Duration,
298
    rhs: Google_Protobuf_Duration
299
0
) -> Google_Protobuf_Duration {
300
0
    let (s, n) = normalizeForDuration(
301
0
        seconds: lhs.seconds - rhs.seconds,
302
0
        nanos: lhs.nanos - rhs.nanos
303
0
    )
304
0
    return Google_Protobuf_Duration(seconds: s, nanos: n)
305
0
}
306
307
public func - (
308
    lhs: Google_Protobuf_Timestamp,
309
    rhs: Google_Protobuf_Timestamp
310
0
) -> Google_Protobuf_Duration {
311
0
    let (s, n) = normalizeForDuration(
312
0
        seconds: lhs.seconds - rhs.seconds,
313
0
        nanos: lhs.nanos - rhs.nanos
314
0
    )
315
0
    return Google_Protobuf_Duration(seconds: s, nanos: n)
316
0
}