/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 | | } |