/src/simdutf/fuzz/base64_details.cpp
Line | Count | Source |
1 | | // Fuzz tests for base64_to_binary_details (both char and char16_t variants). |
2 | | // |
3 | | // base64_to_binary_details returns a full_result that carries both input_count |
4 | | // and output_count even on errors, unlike base64_to_binary which only returns |
5 | | // one of them. This provides richer checking opportunities. |
6 | | // |
7 | | // Invariants checked: |
8 | | // 1. All implementations agree on full_result (differential testing). |
9 | | // 2. input_count <= input length and output_count <= maximal_binary_length. |
10 | | // 3. Consistency: full_result cast to result matches base64_to_binary result. |
11 | | // 4. On success, the output bytes from both functions are identical. |
12 | | // 5. padding_error is consistent across implementations. |
13 | | |
14 | | #include <cstddef> |
15 | | #include <cstdint> |
16 | | #include <cstdlib> |
17 | | #include <iostream> |
18 | | #include <span> |
19 | | #include <vector> |
20 | | |
21 | | #include "helpers/common.h" |
22 | | #include "simdutf.h" |
23 | | |
24 | | constexpr std::array options = { |
25 | | simdutf::base64_default, |
26 | | simdutf::base64_url, |
27 | | simdutf::base64_default_no_padding, |
28 | | simdutf::base64_url_with_padding, |
29 | | }; |
30 | | |
31 | | constexpr std::array last_chunk = { |
32 | | simdutf::last_chunk_handling_options::loose, |
33 | | simdutf::last_chunk_handling_options::strict, |
34 | | simdutf::last_chunk_handling_options::stop_before_partial, |
35 | | }; |
36 | | |
37 | | struct comparable_full_result { |
38 | | simdutf::error_code error{}; |
39 | | std::size_t input_count{}; |
40 | | std::size_t output_count{}; |
41 | | bool padding_error{}; |
42 | | std::string output_hash; // hash of output, only set on SUCCESS |
43 | | |
44 | | auto operator<=>(const comparable_full_result&) const = default; |
45 | | }; |
46 | | |
47 | 0 | std::ostream& operator<<(std::ostream& os, const comparable_full_result& r) { |
48 | 0 | os << "[error=" << r.error << " in=" << r.input_count |
49 | 0 | << " out=" << r.output_count << " pad_err=" << r.padding_error |
50 | 0 | << " hash=" << r.output_hash << "]"; |
51 | 0 | return os; |
52 | 0 | } |
53 | | |
54 | | template <typename FromChar> |
55 | | static void test_details(std::span<const FromChar> input, |
56 | | simdutf::base64_options opt, |
57 | 3.79k | simdutf::last_chunk_handling_options lco) { |
58 | 3.79k | const auto implementations = get_supported_implementations(); |
59 | 3.79k | if (implementations.empty()) { |
60 | 0 | return; |
61 | 0 | } |
62 | | |
63 | 3.79k | std::vector<comparable_full_result> results; |
64 | 3.79k | results.reserve(implementations.size()); |
65 | | |
66 | 11.3k | for (auto impl : implementations) { |
67 | | // Use a fresh buffer for base64_to_binary_details. |
68 | 11.3k | const std::size_t maxbinary = |
69 | 11.3k | impl->maximal_binary_length_from_base64(input.data(), input.size()); |
70 | 11.3k | std::vector<char> out_details(maxbinary); |
71 | | |
72 | 11.3k | const simdutf::full_result fr = impl->base64_to_binary_details( |
73 | 11.3k | input.data(), input.size(), out_details.data(), opt, lco); |
74 | | |
75 | | // Bounds checks. |
76 | 11.3k | if (fr.input_count > input.size()) { |
77 | 0 | std::cerr << "base64_to_binary_details: input_count=" << fr.input_count |
78 | 0 | << " > input.size()=" << input.size() |
79 | 0 | << " impl=" << impl->name() << "\n"; |
80 | 0 | std::abort(); |
81 | 0 | } |
82 | 11.3k | if (fr.output_count > maxbinary) { |
83 | 0 | std::cerr << "base64_to_binary_details: output_count=" << fr.output_count |
84 | 0 | << " > maxbinary=" << maxbinary << " impl=" << impl->name() |
85 | 0 | << "\n"; |
86 | 0 | std::abort(); |
87 | 0 | } |
88 | | |
89 | | // Consistency check: full_result cast to result must equal |
90 | | // base64_to_binary. |
91 | 11.3k | std::vector<char> out_plain(maxbinary); |
92 | 11.3k | const simdutf::result r = impl->base64_to_binary( |
93 | 11.3k | input.data(), input.size(), out_plain.data(), opt, lco); |
94 | 11.3k | const simdutf::result r_from_fr = static_cast<simdutf::result>(fr); |
95 | | |
96 | 11.3k | if (r != r_from_fr) { |
97 | 0 | std::cerr << "base64_to_binary_details vs base64_to_binary inconsistency" |
98 | 0 | << " impl=" << impl->name() |
99 | 0 | << " base64_to_binary=[error=" << r.error |
100 | 0 | << " count=" << r.count << "]" |
101 | 0 | << " full_result cast=[error=" << r_from_fr.error |
102 | 0 | << " count=" << r_from_fr.count << "]\n"; |
103 | 0 | std::abort(); |
104 | 0 | } |
105 | | |
106 | | // On success, output bytes from both functions must match. |
107 | 11.3k | if (fr.error == simdutf::error_code::SUCCESS) { |
108 | 3.91k | if (fr.output_count != r.count) { |
109 | 0 | std::cerr << "base64_to_binary_details: SUCCESS but output_count=" |
110 | 0 | << fr.output_count << " != r.count=" << r.count |
111 | 0 | << " impl=" << impl->name() << "\n"; |
112 | 0 | std::abort(); |
113 | 0 | } |
114 | 3.91k | const auto span_details = |
115 | 3.91k | std::span(out_details).subspan(0, fr.output_count); |
116 | 3.91k | const auto span_plain = std::span(out_plain).subspan(0, r.count); |
117 | 3.91k | if (!std::ranges::equal(span_details, span_plain)) { |
118 | 0 | std::cerr << "base64_to_binary_details: output bytes differ from " |
119 | 0 | "base64_to_binary on SUCCESS" |
120 | 0 | << " impl=" << impl->name() << "\n"; |
121 | 0 | std::abort(); |
122 | 0 | } |
123 | 3.91k | } |
124 | | |
125 | | // Store result for cross-implementation comparison. |
126 | | // |
127 | | // output_count on error is implementation-defined: SIMD implementations |
128 | | // process input in different chunk widths and may write different numbers |
129 | | // of output bytes before detecting an invalid character. Only compare |
130 | | // output_count (and output_hash) when the conversion succeeds. |
131 | | // |
132 | | // input_count on success is also implementation-defined: with |
133 | | // stop_before_partial and trailing ignored whitespace, some implementations |
134 | | // include the trailing whitespace in input_count and others do not. The |
135 | | // full_result::operator result() cast only uses output_count on success |
136 | | // (input_count is not part of the base contract there). Only compare |
137 | | // input_count when the conversion fails, where it carries the error |
138 | | // position (which must be deterministic). |
139 | 11.3k | comparable_full_result cfr; |
140 | 11.3k | cfr.error = fr.error; |
141 | 11.3k | cfr.padding_error = fr.padding_error; |
142 | 11.3k | if (fr.error == simdutf::error_code::SUCCESS) { |
143 | 3.91k | cfr.output_count = fr.output_count; |
144 | 3.91k | cfr.output_hash = FNV1A_hash::as_str( |
145 | 3.91k | std::span(out_details).subspan(0, fr.output_count)); |
146 | 7.46k | } else { |
147 | 7.46k | cfr.input_count = fr.input_count; |
148 | 7.46k | } |
149 | 11.3k | results.push_back(std::move(cfr)); |
150 | 11.3k | } |
151 | | |
152 | | // All implementations must agree on the full_result. |
153 | 7.58k | auto neq = [](const auto& a, const auto& b) { return a != b; };base64_details.cpp:auto test_details<char>(std::__1::span<char const, 18446744073709551615ul>, simdutf::base64_options, simdutf::last_chunk_handling_options)::{lambda(auto:1 const&, auto:2 const&)#1}::operator()<comparable_full_result, {lambda(auto:1 const&, auto:2 const&)#1}::operator()>(comparable_full_result const&, {lambda(auto:1 const&, auto:2 const&)#1}::operator() const&) constLine | Count | Source | 153 | 3.78k | auto neq = [](const auto& a, const auto& b) { return a != b; }; |
base64_details.cpp:auto test_details<char16_t>(std::__1::span<char16_t const, 18446744073709551615ul>, simdutf::base64_options, simdutf::last_chunk_handling_options)::{lambda(auto:1 const&, auto:2 const&)#1}::operator()<comparable_full_result, {lambda(auto:1 const&, auto:2 const&)#1}::operator()>(comparable_full_result const&, {lambda(auto:1 const&, auto:2 const&)#1}::operator() const&) constLine | Count | Source | 153 | 3.80k | auto neq = [](const auto& a, const auto& b) { return a != b; }; |
|
154 | 3.79k | if (std::ranges::adjacent_find(results, neq) != results.end()) { |
155 | 0 | std::cerr << "base64_to_binary_details: implementations disagree\n"; |
156 | 0 | for (std::size_t i = 0; i < implementations.size(); ++i) { |
157 | 0 | std::cerr << " " << implementations[i]->name() << ": " << results[i] |
158 | 0 | << "\n"; |
159 | 0 | } |
160 | 0 | std::abort(); |
161 | 0 | } |
162 | 3.79k | } base64_details.cpp:void test_details<char>(std::__1::span<char const, 18446744073709551615ul>, simdutf::base64_options, simdutf::last_chunk_handling_options) Line | Count | Source | 57 | 1.89k | simdutf::last_chunk_handling_options lco) { | 58 | 1.89k | const auto implementations = get_supported_implementations(); | 59 | 1.89k | if (implementations.empty()) { | 60 | 0 | return; | 61 | 0 | } | 62 | | | 63 | 1.89k | std::vector<comparable_full_result> results; | 64 | 1.89k | results.reserve(implementations.size()); | 65 | | | 66 | 5.67k | for (auto impl : implementations) { | 67 | | // Use a fresh buffer for base64_to_binary_details. | 68 | 5.67k | const std::size_t maxbinary = | 69 | 5.67k | impl->maximal_binary_length_from_base64(input.data(), input.size()); | 70 | 5.67k | std::vector<char> out_details(maxbinary); | 71 | | | 72 | 5.67k | const simdutf::full_result fr = impl->base64_to_binary_details( | 73 | 5.67k | input.data(), input.size(), out_details.data(), opt, lco); | 74 | | | 75 | | // Bounds checks. | 76 | 5.67k | if (fr.input_count > input.size()) { | 77 | 0 | std::cerr << "base64_to_binary_details: input_count=" << fr.input_count | 78 | 0 | << " > input.size()=" << input.size() | 79 | 0 | << " impl=" << impl->name() << "\n"; | 80 | 0 | std::abort(); | 81 | 0 | } | 82 | 5.67k | if (fr.output_count > maxbinary) { | 83 | 0 | std::cerr << "base64_to_binary_details: output_count=" << fr.output_count | 84 | 0 | << " > maxbinary=" << maxbinary << " impl=" << impl->name() | 85 | 0 | << "\n"; | 86 | 0 | std::abort(); | 87 | 0 | } | 88 | | | 89 | | // Consistency check: full_result cast to result must equal | 90 | | // base64_to_binary. | 91 | 5.67k | std::vector<char> out_plain(maxbinary); | 92 | 5.67k | const simdutf::result r = impl->base64_to_binary( | 93 | 5.67k | input.data(), input.size(), out_plain.data(), opt, lco); | 94 | 5.67k | const simdutf::result r_from_fr = static_cast<simdutf::result>(fr); | 95 | | | 96 | 5.67k | if (r != r_from_fr) { | 97 | 0 | std::cerr << "base64_to_binary_details vs base64_to_binary inconsistency" | 98 | 0 | << " impl=" << impl->name() | 99 | 0 | << " base64_to_binary=[error=" << r.error | 100 | 0 | << " count=" << r.count << "]" | 101 | 0 | << " full_result cast=[error=" << r_from_fr.error | 102 | 0 | << " count=" << r_from_fr.count << "]\n"; | 103 | 0 | std::abort(); | 104 | 0 | } | 105 | | | 106 | | // On success, output bytes from both functions must match. | 107 | 5.67k | if (fr.error == simdutf::error_code::SUCCESS) { | 108 | 2.24k | if (fr.output_count != r.count) { | 109 | 0 | std::cerr << "base64_to_binary_details: SUCCESS but output_count=" | 110 | 0 | << fr.output_count << " != r.count=" << r.count | 111 | 0 | << " impl=" << impl->name() << "\n"; | 112 | 0 | std::abort(); | 113 | 0 | } | 114 | 2.24k | const auto span_details = | 115 | 2.24k | std::span(out_details).subspan(0, fr.output_count); | 116 | 2.24k | const auto span_plain = std::span(out_plain).subspan(0, r.count); | 117 | 2.24k | if (!std::ranges::equal(span_details, span_plain)) { | 118 | 0 | std::cerr << "base64_to_binary_details: output bytes differ from " | 119 | 0 | "base64_to_binary on SUCCESS" | 120 | 0 | << " impl=" << impl->name() << "\n"; | 121 | 0 | std::abort(); | 122 | 0 | } | 123 | 2.24k | } | 124 | | | 125 | | // Store result for cross-implementation comparison. | 126 | | // | 127 | | // output_count on error is implementation-defined: SIMD implementations | 128 | | // process input in different chunk widths and may write different numbers | 129 | | // of output bytes before detecting an invalid character. Only compare | 130 | | // output_count (and output_hash) when the conversion succeeds. | 131 | | // | 132 | | // input_count on success is also implementation-defined: with | 133 | | // stop_before_partial and trailing ignored whitespace, some implementations | 134 | | // include the trailing whitespace in input_count and others do not. The | 135 | | // full_result::operator result() cast only uses output_count on success | 136 | | // (input_count is not part of the base contract there). Only compare | 137 | | // input_count when the conversion fails, where it carries the error | 138 | | // position (which must be deterministic). | 139 | 5.67k | comparable_full_result cfr; | 140 | 5.67k | cfr.error = fr.error; | 141 | 5.67k | cfr.padding_error = fr.padding_error; | 142 | 5.67k | if (fr.error == simdutf::error_code::SUCCESS) { | 143 | 2.24k | cfr.output_count = fr.output_count; | 144 | 2.24k | cfr.output_hash = FNV1A_hash::as_str( | 145 | 2.24k | std::span(out_details).subspan(0, fr.output_count)); | 146 | 3.43k | } else { | 147 | 3.43k | cfr.input_count = fr.input_count; | 148 | 3.43k | } | 149 | 5.67k | results.push_back(std::move(cfr)); | 150 | 5.67k | } | 151 | | | 152 | | // All implementations must agree on the full_result. | 153 | 1.89k | auto neq = [](const auto& a, const auto& b) { return a != b; }; | 154 | 1.89k | if (std::ranges::adjacent_find(results, neq) != results.end()) { | 155 | 0 | std::cerr << "base64_to_binary_details: implementations disagree\n"; | 156 | 0 | for (std::size_t i = 0; i < implementations.size(); ++i) { | 157 | 0 | std::cerr << " " << implementations[i]->name() << ": " << results[i] | 158 | 0 | << "\n"; | 159 | 0 | } | 160 | 0 | std::abort(); | 161 | 0 | } | 162 | 1.89k | } |
base64_details.cpp:void test_details<char16_t>(std::__1::span<char16_t const, 18446744073709551615ul>, simdutf::base64_options, simdutf::last_chunk_handling_options) Line | Count | Source | 57 | 1.90k | simdutf::last_chunk_handling_options lco) { | 58 | 1.90k | const auto implementations = get_supported_implementations(); | 59 | 1.90k | if (implementations.empty()) { | 60 | 0 | return; | 61 | 0 | } | 62 | | | 63 | 1.90k | std::vector<comparable_full_result> results; | 64 | 1.90k | results.reserve(implementations.size()); | 65 | | | 66 | 5.70k | for (auto impl : implementations) { | 67 | | // Use a fresh buffer for base64_to_binary_details. | 68 | 5.70k | const std::size_t maxbinary = | 69 | 5.70k | impl->maximal_binary_length_from_base64(input.data(), input.size()); | 70 | 5.70k | std::vector<char> out_details(maxbinary); | 71 | | | 72 | 5.70k | const simdutf::full_result fr = impl->base64_to_binary_details( | 73 | 5.70k | input.data(), input.size(), out_details.data(), opt, lco); | 74 | | | 75 | | // Bounds checks. | 76 | 5.70k | if (fr.input_count > input.size()) { | 77 | 0 | std::cerr << "base64_to_binary_details: input_count=" << fr.input_count | 78 | 0 | << " > input.size()=" << input.size() | 79 | 0 | << " impl=" << impl->name() << "\n"; | 80 | 0 | std::abort(); | 81 | 0 | } | 82 | 5.70k | if (fr.output_count > maxbinary) { | 83 | 0 | std::cerr << "base64_to_binary_details: output_count=" << fr.output_count | 84 | 0 | << " > maxbinary=" << maxbinary << " impl=" << impl->name() | 85 | 0 | << "\n"; | 86 | 0 | std::abort(); | 87 | 0 | } | 88 | | | 89 | | // Consistency check: full_result cast to result must equal | 90 | | // base64_to_binary. | 91 | 5.70k | std::vector<char> out_plain(maxbinary); | 92 | 5.70k | const simdutf::result r = impl->base64_to_binary( | 93 | 5.70k | input.data(), input.size(), out_plain.data(), opt, lco); | 94 | 5.70k | const simdutf::result r_from_fr = static_cast<simdutf::result>(fr); | 95 | | | 96 | 5.70k | if (r != r_from_fr) { | 97 | 0 | std::cerr << "base64_to_binary_details vs base64_to_binary inconsistency" | 98 | 0 | << " impl=" << impl->name() | 99 | 0 | << " base64_to_binary=[error=" << r.error | 100 | 0 | << " count=" << r.count << "]" | 101 | 0 | << " full_result cast=[error=" << r_from_fr.error | 102 | 0 | << " count=" << r_from_fr.count << "]\n"; | 103 | 0 | std::abort(); | 104 | 0 | } | 105 | | | 106 | | // On success, output bytes from both functions must match. | 107 | 5.70k | if (fr.error == simdutf::error_code::SUCCESS) { | 108 | 1.67k | if (fr.output_count != r.count) { | 109 | 0 | std::cerr << "base64_to_binary_details: SUCCESS but output_count=" | 110 | 0 | << fr.output_count << " != r.count=" << r.count | 111 | 0 | << " impl=" << impl->name() << "\n"; | 112 | 0 | std::abort(); | 113 | 0 | } | 114 | 1.67k | const auto span_details = | 115 | 1.67k | std::span(out_details).subspan(0, fr.output_count); | 116 | 1.67k | const auto span_plain = std::span(out_plain).subspan(0, r.count); | 117 | 1.67k | if (!std::ranges::equal(span_details, span_plain)) { | 118 | 0 | std::cerr << "base64_to_binary_details: output bytes differ from " | 119 | 0 | "base64_to_binary on SUCCESS" | 120 | 0 | << " impl=" << impl->name() << "\n"; | 121 | 0 | std::abort(); | 122 | 0 | } | 123 | 1.67k | } | 124 | | | 125 | | // Store result for cross-implementation comparison. | 126 | | // | 127 | | // output_count on error is implementation-defined: SIMD implementations | 128 | | // process input in different chunk widths and may write different numbers | 129 | | // of output bytes before detecting an invalid character. Only compare | 130 | | // output_count (and output_hash) when the conversion succeeds. | 131 | | // | 132 | | // input_count on success is also implementation-defined: with | 133 | | // stop_before_partial and trailing ignored whitespace, some implementations | 134 | | // include the trailing whitespace in input_count and others do not. The | 135 | | // full_result::operator result() cast only uses output_count on success | 136 | | // (input_count is not part of the base contract there). Only compare | 137 | | // input_count when the conversion fails, where it carries the error | 138 | | // position (which must be deterministic). | 139 | 5.70k | comparable_full_result cfr; | 140 | 5.70k | cfr.error = fr.error; | 141 | 5.70k | cfr.padding_error = fr.padding_error; | 142 | 5.70k | if (fr.error == simdutf::error_code::SUCCESS) { | 143 | 1.67k | cfr.output_count = fr.output_count; | 144 | 1.67k | cfr.output_hash = FNV1A_hash::as_str( | 145 | 1.67k | std::span(out_details).subspan(0, fr.output_count)); | 146 | 4.02k | } else { | 147 | 4.02k | cfr.input_count = fr.input_count; | 148 | 4.02k | } | 149 | 5.70k | results.push_back(std::move(cfr)); | 150 | 5.70k | } | 151 | | | 152 | | // All implementations must agree on the full_result. | 153 | 1.90k | auto neq = [](const auto& a, const auto& b) { return a != b; }; | 154 | 1.90k | if (std::ranges::adjacent_find(results, neq) != results.end()) { | 155 | 0 | std::cerr << "base64_to_binary_details: implementations disagree\n"; | 156 | 0 | for (std::size_t i = 0; i < implementations.size(); ++i) { | 157 | 0 | std::cerr << " " << implementations[i]->name() << ": " << results[i] | 158 | 0 | << "\n"; | 159 | 0 | } | 160 | 0 | std::abort(); | 161 | 0 | } | 162 | 1.90k | } |
|
163 | | |
164 | 3.79k | extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { |
165 | 3.79k | constexpr auto optionbytes = 4u; |
166 | 3.79k | static_assert(optionbytes % 2 == 0, |
167 | 3.79k | "optionbytes must be even to avoid misaligned char16 pointers"); |
168 | 3.79k | if (size < optionbytes) { |
169 | 2 | return 0; |
170 | 2 | } |
171 | | |
172 | 3.79k | constexpr auto Ncases = 2u; |
173 | 3.79k | constexpr auto actionmask = std::bit_ceil(Ncases) - 1; |
174 | 3.79k | const auto action = data[0] & actionmask; |
175 | | |
176 | 3.79k | const auto opt = [](auto index) { |
177 | 3.79k | if (index >= options.size()) { |
178 | 0 | return options[0]; |
179 | 0 | } |
180 | 3.79k | return options[index]; |
181 | 3.79k | }(data[1] & (std::bit_ceil(options.size()) - 1)); |
182 | | |
183 | 3.79k | const auto lco = |
184 | 3.79k | (opt == simdutf::base64_url || opt == simdutf::base64_default_no_padding) |
185 | 3.79k | ? simdutf::last_chunk_handling_options::loose |
186 | 3.79k | : [](auto index) { |
187 | 2.74k | if (index >= last_chunk.size()) { |
188 | 289 | return last_chunk[0]; |
189 | 289 | } |
190 | 2.45k | return last_chunk[index]; |
191 | 2.74k | }(data[2] & (std::bit_ceil(last_chunk.size()) - 1)); |
192 | | |
193 | | // data[3] reserved for future use / alignment. |
194 | | |
195 | 3.79k | data += optionbytes; |
196 | 3.79k | size -= optionbytes; |
197 | | |
198 | 3.79k | switch (action) { |
199 | 1.89k | case 0: { |
200 | 1.89k | const std::span<const char> chardata{reinterpret_cast<const char*>(data), |
201 | 1.89k | size}; |
202 | 1.89k | test_details(chardata, opt, lco); |
203 | 1.89k | } break; |
204 | 1.90k | case 1: { |
205 | 1.90k | const std::span<const char16_t> chardata{ |
206 | 1.90k | reinterpret_cast<const char16_t*>(data), size / sizeof(char16_t)}; |
207 | 1.90k | test_details(chardata, opt, lco); |
208 | 1.90k | } break; |
209 | 3.79k | } |
210 | | |
211 | 3.79k | return 0; |
212 | 3.79k | } |