Coverage Report

Created: 2026-10-17 07:16

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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&) const
Line
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&) const
Line
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
}