Coverage Report

Created: 2025-07-04 06:57

/src/swift-protobuf/FuzzTesting/Sources/FuzzCommon/Options.swift
Line
Count
Source (jump to first uncovered line)
1
// Copyright (c) 2014 - 2024 Apple Inc. and the project authors
2
// Licensed under Apache License v2.0 with Runtime Library Exception
3
//
4
// See LICENSE.txt for license information:
5
// https://github.com/apple/swift-protobuf/blob/main/LICENSE.txt
6
//
7
// -----------------------------------------------------------------------------
8
9
import Foundation
10
import SwiftProtobuf
11
12
public enum FuzzOption<T: SupportsFuzzOptions> {
13
    case boolean(WritableKeyPath<T, Bool>)
14
    case byte(WritableKeyPath<T, Int>, mod: UInt8 = .max)
15
}
16
17
public protocol SupportsFuzzOptions {
18
    static var fuzzOptionsList: [FuzzOption<Self>] { get }
19
    init()
20
}
21
22
extension SupportsFuzzOptions {
23
    public static func extractOptions(
24
        _ start: UnsafeRawPointer,
25
        _ count: Int
26
111k
    ) -> (Self, UnsafeRawBufferPointer)? {
27
111k
        var start = start
28
111k
        let initialCount = count
29
111k
        var count = count
30
111k
        var options = Self()
31
111k
        let reportInfo = ProcessInfo.processInfo.environment["DUMP_DECODE_INFO"] == "1"
32
111k
33
111k
        // No format can start with zero (invalid tag, not really UTF-8), so use that to
34
111k
        // indicate there are decoding options. The one case that can start with a zero
35
111k
        // would be length delimited binary, but since that's a zero length message, we
36
111k
        // can go ahead and use that one also.
37
111k
        guard count >= 2, start.loadUnaligned(as: UInt8.self) == 0 else {
38
96.6k
            if reportInfo {
39
0
                print("No options to decode")
40
96.6k
            }
41
96.6k
            return (options, UnsafeRawBufferPointer(start: start, count: count))
42
96.6k
        }
43
15.2k
44
15.2k
        // Step over the zero
45
15.2k
        start += 1
46
15.2k
        count -= 1
47
15.2k
48
15.2k
        var optionsBits = start.loadUnaligned(as: UInt8.self)
49
15.2k
        start += 1
50
15.2k
        count -= 1
51
15.2k
        var bit = 0
52
35.9k
        for opt in fuzzOptionsList {
53
35.9k
            var isSet = optionsBits & (1 << bit) != 0
54
35.9k
            if bit == 7 {
55
0
                // About the use the last bit of this byte, to allow more options in
56
0
                // the future, use this bit to indicate reading another byte.
57
0
                guard isSet else {
58
0
                    // No continuation, just return whatever we got.
59
0
                    bit = 8
60
0
                    break
61
0
                }
62
0
                guard count >= 1 else {
63
0
                    return nil  // No data left to read bits
64
0
                }
65
0
                optionsBits = start.loadUnaligned(as: UInt8.self)
66
0
                start += 1
67
0
                count -= 1
68
0
                bit = 0
69
0
                isSet = optionsBits & (1 << bit) != 0
70
35.9k
            }
71
35.9k
72
35.9k
            switch opt {
73
35.9k
            case .boolean(let keypath):
74
20.6k
                options[keyPath: keypath] = isSet
75
35.9k
            case .byte(let keypath, let mod):
76
15.2k
                assert(mod >= 1 && mod <= UInt8.max)
77
15.2k
                if isSet {
78
1.31k
                    guard count >= 1 else {
79
6
                        return nil  // No more bytes to get a value, fail
80
1.30k
                    }
81
1.30k
                    let value = start.loadUnaligned(as: UInt8.self)
82
1.30k
                    start += 1
83
1.30k
                    count -= 1
84
1.30k
                    options[keyPath: keypath] = Int(value % mod)
85
35.9k
                }
86
35.9k
            }
87
35.9k
            bit += 1
88
35.9k
        }
89
15.2k
        // Ensure the any remaining bits are zero so they can be used in the future
90
101k
        while bit < 8 {
91
86.2k
            if optionsBits & (1 << bit) != 0 {
92
30
                return nil
93
86.2k
            }
94
86.2k
            bit += 1
95
86.2k
        }
96
15.2k
97
15.2k
        if reportInfo {
98
0
            print("\(initialCount - count) bytes consumed off front for options: \(options)")
99
15.2k
        }
100
15.2k
        return (options, UnsafeRawBufferPointer(start: start, count: count))
101
15.2k
    }
102
103
}
104
105
extension BinaryDecodingOptions: SupportsFuzzOptions {
106
216
    public static var fuzzOptionsList: [FuzzOption<Self>] {
107
216
        [
108
216
            // NOTE: Do not reorder these in the future as it invalidates all
109
216
            // existing cases.
110
216
111
216
            // The default depth is 100, so limit outselves to modding by 8 to
112
216
            // avoid allowing larger depths that could timeout.
113
216
            .byte(\.messageDepthLimit, mod: 8),
114
216
            .boolean(\.discardUnknownFields),
115
216
        ]
116
216
    }
117
}
118
119
extension JSONDecodingOptions: SupportsFuzzOptions {
120
9.70k
    public static var fuzzOptionsList: [FuzzOption<Self>] {
121
9.70k
        [
122
9.70k
            // NOTE: Do not reorder these in the future as it invalidates all
123
9.70k
            // existing cases.
124
9.70k
125
9.70k
            // The default depth is 100, so limit outselves to modding by 8 to
126
9.70k
            // avoid allowing larger depths that could timeout.
127
9.70k
            .byte(\.messageDepthLimit, mod: 8),
128
9.70k
            .boolean(\.ignoreUnknownFields),
129
9.70k
        ]
130
9.70k
    }
131
}
132
133
extension TextFormatDecodingOptions: SupportsFuzzOptions {
134
5.37k
    public static var fuzzOptionsList: [FuzzOption<Self>] {
135
5.37k
        [
136
5.37k
            // NOTE: Do not reorder these in the future as it invalidates all
137
5.37k
            // existing cases.
138
5.37k
139
5.37k
            // The default depth is 100, so limit outselves to modding by 8 to
140
5.37k
            // avoid allowing larger depths that could timeout.
141
5.37k
            .byte(\.messageDepthLimit, mod: 8),
142
5.37k
            .boolean(\.ignoreUnknownFields),
143
5.37k
            .boolean(\.ignoreUnknownExtensionFields),
144
5.37k
        ]
145
5.37k
    }
146
}