Coverage Report

Created: 2026-09-28 06:13

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/ada-url/fuzz/url_pattern.cc
Line
Count
Source
1
#include <fuzzer/FuzzedDataProvider.h>
2
3
#include <memory>
4
#include <string>
5
6
#include "ada.cpp"
7
#include "ada.h"
8
9
using regex_provider = ada::url_pattern_regex::std_regex_provider;
10
11
127k
void exercise_result(auto result) {
12
127k
  (void)result.get_protocol();
13
127k
  (void)result.get_username();
14
127k
  (void)result.get_password();
15
127k
  (void)result.get_hostname();
16
127k
  (void)result.get_port();
17
127k
  (void)result.get_pathname();
18
127k
  (void)result.get_search();
19
127k
  (void)result.get_hash();
20
127k
  (void)result.ignore_case();
21
127k
  (void)result.has_regexp_groups();
22
127k
}
23
24
// Shared helper: walk every field of a url_pattern_result.
25
45.4k
static void exercise_match_result(const ada::url_pattern_result& match) {
26
45.4k
  volatile size_t len = 0;
27
363k
  auto exercise_component = [&len](const ada::url_pattern_component_result& c) {
28
363k
    len += c.input.size();
29
363k
    for (const auto& [k, v] : c.groups) {
30
358k
      len += k.size();
31
358k
      if (v.has_value()) len += v->size();
32
358k
    }
33
363k
  };
34
45.4k
  exercise_component(match.protocol);
35
45.4k
  exercise_component(match.username);
36
45.4k
  exercise_component(match.password);
37
45.4k
  exercise_component(match.hostname);
38
45.4k
  exercise_component(match.port);
39
45.4k
  exercise_component(match.pathname);
40
45.4k
  exercise_component(match.search);
41
45.4k
  exercise_component(match.hash);
42
  // Exercise the 'inputs' vector (each element is a url_pattern_input
43
  // variant holding either a string_view or url_pattern_init).
44
54.3k
  for (const auto& inp : match.inputs) {
45
54.3k
    if (std::holds_alternative<std::string_view>(inp)) {
46
26.8k
      len += std::get<std::string_view>(inp).size();
47
26.8k
    }
48
54.3k
  }
49
45.4k
  (void)len;
50
45.4k
}
51
52
// Exercise exec() and test() on a parsed url_pattern with an ASCII input.
53
// We restrict inputs to ASCII to avoid catastrophic regex backtracking.
54
static void exercise_exec_and_test(ada::url_pattern<regex_provider>& pattern,
55
                                   const std::string& test_input,
56
127k
                                   const std::string& test_base) {
57
127k
  std::string_view test_view(test_input.data(), test_input.size());
58
59
  // exec() and test() must agree: exec finds a match iff test returns true.
60
  // Both operate on the same input so their answers must be consistent.
61
127k
  auto exec_result = pattern.exec(test_view, nullptr);
62
127k
  auto test_result = pattern.test(test_view, nullptr);
63
64
127k
  bool exec_matched = exec_result && exec_result->has_value();
65
127k
  bool test_matched = test_result && *test_result;
66
67
127k
  if (exec_matched != test_matched) {
68
0
    printf(
69
0
        "exec/test inconsistency on input '%s': exec_matched=%d "
70
0
        "test_matched=%d\n",
71
0
        test_input.c_str(), exec_matched, test_matched);
72
0
    abort();
73
0
  }
74
75
127k
  if (exec_result && exec_result->has_value()) {
76
8.95k
    exercise_match_result(**exec_result);
77
8.95k
  }
78
79
  // test() with base URL
80
127k
  if (!test_base.empty()) {
81
127k
    std::string_view base_view(test_base.data(), test_base.size());
82
127k
    auto test_result_with_base = pattern.test(test_view, &base_view);
83
127k
    auto exec_with_base = pattern.exec(test_view, &base_view);
84
85
127k
    bool exec_base_matched = exec_with_base && exec_with_base->has_value();
86
127k
    bool test_base_matched = test_result_with_base && *test_result_with_base;
87
88
127k
    if (exec_base_matched != test_base_matched) {
89
0
      printf(
90
0
          "exec/test inconsistency with base on input '%s': "
91
0
          "exec_matched=%d test_matched=%d\n",
92
0
          test_input.c_str(), exec_base_matched, test_base_matched);
93
0
      abort();
94
0
    }
95
96
127k
    if (exec_with_base && exec_with_base->has_value()) {
97
8.95k
      exercise_match_result(**exec_with_base);
98
8.95k
    }
99
127k
  }
100
101
  // test() with url_pattern_init input (sets only the pathname component)
102
127k
  ada::url_pattern_init init_input{};
103
127k
  init_input.pathname = test_input;
104
127k
  auto test_with_init = pattern.test(init_input, nullptr);
105
127k
  auto exec_with_init = pattern.exec(init_input, nullptr);
106
  // exec and test must agree on the init-based input too.
107
127k
  if ((test_with_init && *test_with_init) !=
108
127k
      (exec_with_init && exec_with_init->has_value())) {
109
0
    printf("exec/test inconsistency on url_pattern_init input\n");
110
0
    abort();
111
0
  }
112
127k
  if (exec_with_init && exec_with_init->has_value()) {
113
27.5k
    exercise_match_result(**exec_with_init);
114
27.5k
  }
115
116
  // test_components() — tests each URL component individually
117
127k
  {
118
127k
    std::string_view sv(test_input.data(), test_input.size());
119
127k
    auto parsed = ada::parse<ada::url_aggregator>(sv);
120
127k
    if (parsed) {
121
38.1k
      volatile bool tc = pattern.test_components(
122
38.1k
          std::string(parsed->get_protocol()),
123
38.1k
          std::string(parsed->get_username()),
124
38.1k
          std::string(parsed->get_password()),
125
38.1k
          std::string(parsed->get_hostname()), std::string(parsed->get_port()),
126
38.1k
          std::string(parsed->get_pathname()),
127
38.1k
          std::string(parsed->get_search()), std::string(parsed->get_hash()));
128
38.1k
      (void)tc;
129
38.1k
    }
130
127k
  }
131
132
  // match() — the internal method underlying exec(); must not crash.
133
127k
  auto match_result = pattern.match(test_view, nullptr);
134
127k
  (void)match_result;
135
127k
}
136
137
14.7k
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
138
451k
  auto to_ascii = [](const std::string& source) -> std::string {
139
451k
    std::string result;
140
451k
    result.reserve(source.size());
141
451k
    for (char c : source) {
142
206k
      result.push_back(static_cast<unsigned char>(c) % 128);
143
206k
    }
144
451k
    return result;
145
451k
  };
146
14.7k
  FuzzedDataProvider fdp(data, size);
147
  // We do not want to trigger arbitrary regex matching.
148
14.7k
  std::string source_1 = "/" + to_ascii(fdp.ConsumeRandomLengthString(50)) +
149
14.7k
                         "/" + to_ascii(fdp.ConsumeRandomLengthString(50));
150
14.7k
  std::string base_source_1 = "/" +
151
14.7k
                              to_ascii(fdp.ConsumeRandomLengthString(50)) +
152
14.7k
                              "/" + to_ascii(fdp.ConsumeRandomLengthString(50));
153
154
14.7k
  std::string source_2 = "https://ada-url.com/*";
155
14.7k
  std::string base_source_2 = "https://ada-url.com";
156
157
  // Additional test input for exec/test calls (also ASCII-only)
158
14.7k
  std::string test_input = "https://" +
159
14.7k
                           to_ascii(fdp.ConsumeRandomLengthString(30)) + "/" +
160
14.7k
                           to_ascii(fdp.ConsumeRandomLengthString(20));
161
14.7k
  std::string test_base = "https://ada-url.com";
162
163
14.7k
  std::array<std::pair<std::string, std::string>, 2> sources = {{
164
14.7k
      {source_1, base_source_1},
165
14.7k
      {source_2, base_source_2},
166
14.7k
  }};
167
168
29.5k
  for (const auto& [source, base_source] : sources) {
169
    // Without base or options
170
29.5k
    auto result =
171
29.5k
        ada::parse_url_pattern<regex_provider>(source, nullptr, nullptr);
172
29.5k
    if (result) {
173
14.7k
      exercise_result(*result);
174
14.7k
      exercise_exec_and_test(*result, test_input, test_base);
175
14.7k
    }
176
177
    // Testing with base_url
178
29.5k
    std::string_view base_source_view(base_source.data(), base_source.length());
179
29.5k
    auto result_with_base = ada::parse_url_pattern<regex_provider>(
180
29.5k
        source, &base_source_view, nullptr);
181
29.5k
    if (result_with_base) {
182
14.7k
      exercise_result(*result_with_base);
183
14.7k
      exercise_exec_and_test(*result_with_base, test_input, test_base);
184
14.7k
    }
185
186
    // Testing with base_url and options
187
29.5k
    ada::url_pattern_options options{.ignore_case = fdp.ConsumeBool()};
188
29.5k
    auto result_with_base_and_options = ada::parse_url_pattern<regex_provider>(
189
29.5k
        source, &base_source_view, &options);
190
29.5k
    if (result_with_base_and_options) {
191
14.7k
      exercise_result(*result_with_base_and_options);
192
14.7k
      exercise_exec_and_test(*result_with_base_and_options, test_input,
193
14.7k
                             test_base);
194
14.7k
    }
195
196
    // Testing with url_pattern_init and base url.
197
29.5k
    int field_index = fdp.ConsumeIntegralInRange(0, 7);
198
29.5k
    std::string random_value = to_ascii(fdp.ConsumeRandomLengthString(50));
199
29.5k
    ada::url_pattern_init init{};
200
29.5k
    switch (field_index) {
201
20.4k
      case 0:
202
20.4k
        init.protocol = random_value;
203
20.4k
        break;
204
964
      case 1:
205
964
        init.username = random_value;
206
964
        break;
207
1.79k
      case 2:
208
1.79k
        init.password = random_value;
209
1.79k
        break;
210
890
      case 3:
211
890
        init.hostname = random_value;
212
890
        break;
213
2.60k
      case 4:
214
2.60k
        init.port = random_value;
215
2.60k
        break;
216
995
      case 5:
217
995
        init.pathname = random_value;
218
995
        break;
219
842
      case 6:
220
842
        init.search = random_value;
221
842
        break;
222
1.09k
      case 7:
223
1.09k
        init.hash = random_value;
224
1.09k
        break;
225
29.5k
    }
226
29.5k
    auto result_with_init = ada::parse_url_pattern<regex_provider>(
227
29.5k
        init, &base_source_view, nullptr);
228
29.5k
    if (result_with_init) {
229
0
      exercise_result(*result_with_init);
230
0
      exercise_exec_and_test(*result_with_init, test_input, test_base);
231
0
    }
232
233
    // Testing url_pattern_init with ALL fields populated simultaneously
234
29.5k
    ada::url_pattern_init init_all{};
235
29.5k
    init_all.protocol = to_ascii(fdp.ConsumeRandomLengthString(10));
236
29.5k
    init_all.username = to_ascii(fdp.ConsumeRandomLengthString(10));
237
29.5k
    init_all.password = to_ascii(fdp.ConsumeRandomLengthString(10));
238
29.5k
    init_all.hostname = to_ascii(fdp.ConsumeRandomLengthString(20));
239
29.5k
    init_all.port = to_ascii(fdp.ConsumeRandomLengthString(5));
240
29.5k
    init_all.pathname = "/" + to_ascii(fdp.ConsumeRandomLengthString(20));
241
29.5k
    init_all.search = to_ascii(fdp.ConsumeRandomLengthString(10));
242
29.5k
    init_all.hash = to_ascii(fdp.ConsumeRandomLengthString(10));
243
29.5k
    auto result_with_init_all =
244
29.5k
        ada::parse_url_pattern<regex_provider>(init_all, nullptr, nullptr);
245
29.5k
    if (result_with_init_all) {
246
25.5k
      exercise_result(*result_with_init_all);
247
25.5k
      exercise_exec_and_test(*result_with_init_all, test_input, test_base);
248
25.5k
    }
249
250
    // Testing url_pattern_init with the base_url field set.
251
    //
252
    // url_pattern_init::base_url is a completely separate code path from the
253
    // base_url *parameter* of parse_url_pattern. When base_url is embedded
254
    // inside the init struct the spec processes it differently. This field
255
    // was previously never exercised by any fuzzer.
256
29.5k
    {
257
29.5k
      ada::url_pattern_init init_base_url{};
258
29.5k
      init_base_url.pathname =
259
29.5k
          "/" + to_ascii(fdp.ConsumeRandomLengthString(20));
260
29.5k
      init_base_url.base_url = "https://example.com";
261
29.5k
      auto result_base_in_init = ada::parse_url_pattern<regex_provider>(
262
29.5k
          init_base_url, nullptr, nullptr);
263
29.5k
      if (result_base_in_init) {
264
28.2k
        exercise_result(*result_base_in_init);
265
28.2k
        exercise_exec_and_test(*result_base_in_init, test_input, test_base);
266
28.2k
      }
267
268
      // Also fuzz the base_url field itself.
269
29.5k
      ada::url_pattern_init init_fuzz_base{};
270
29.5k
      init_fuzz_base.pathname =
271
29.5k
          "/" + to_ascii(fdp.ConsumeRandomLengthString(15));
272
29.5k
      init_fuzz_base.base_url =
273
29.5k
          "https://" + to_ascii(fdp.ConsumeRandomLengthString(20));
274
29.5k
      auto result_fuzz_base = ada::parse_url_pattern<regex_provider>(
275
29.5k
          init_fuzz_base, nullptr, nullptr);
276
29.5k
      if (result_fuzz_base) {
277
1.31k
        exercise_result(*result_fuzz_base);
278
1.31k
        exercise_exec_and_test(*result_fuzz_base, test_input, test_base);
279
1.31k
      }
280
29.5k
    }
281
282
    // Testing url_pattern_init with a random subset (2–4) of fields set.
283
    //
284
    // The single-field case (switch above) and the all-fields case are covered
285
    // above. Here we pick a random bitmask of fields so the parser sees every
286
    // combination of present/absent components.
287
29.5k
    {
288
29.5k
      uint8_t field_mask = fdp.ConsumeIntegral<uint8_t>();
289
29.5k
      ada::url_pattern_init init_subset{};
290
29.5k
      if (field_mask & 0x01)
291
930
        init_subset.protocol = to_ascii(fdp.ConsumeRandomLengthString(8));
292
29.5k
      if (field_mask & 0x02)
293
881
        init_subset.hostname = to_ascii(fdp.ConsumeRandomLengthString(20));
294
29.5k
      if (field_mask & 0x04)
295
1.00k
        init_subset.port = to_ascii(fdp.ConsumeRandomLengthString(5));
296
29.5k
      if (field_mask & 0x08)
297
1.05k
        init_subset.pathname =
298
1.05k
            "/" + to_ascii(fdp.ConsumeRandomLengthString(20));
299
29.5k
      if (field_mask & 0x10)
300
1.02k
        init_subset.search = to_ascii(fdp.ConsumeRandomLengthString(10));
301
29.5k
      if (field_mask & 0x20)
302
1.10k
        init_subset.hash = to_ascii(fdp.ConsumeRandomLengthString(10));
303
29.5k
      if (field_mask & 0x40)
304
1.11k
        init_subset.username = to_ascii(fdp.ConsumeRandomLengthString(10));
305
29.5k
      if (field_mask & 0x80)
306
444
        init_subset.password = to_ascii(fdp.ConsumeRandomLengthString(10));
307
29.5k
      auto result_subset =
308
29.5k
          ada::parse_url_pattern<regex_provider>(init_subset, nullptr, nullptr);
309
29.5k
      if (result_subset) {
310
28.1k
        exercise_result(*result_subset);
311
28.1k
        exercise_exec_and_test(*result_subset, test_input, test_base);
312
28.1k
      }
313
29.5k
    }
314
29.5k
  }
315
316
14.7k
  return 0;
317
14.7k
}