Coverage Report

Created: 2026-07-16 06:37

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/ada-url/fuzz/serializers.cc
Line
Count
Source
1
#include <fuzzer/FuzzedDataProvider.h>
2
3
#include <array>
4
#include <cassert>
5
#include <cstdio>
6
#include <string>
7
8
#include "ada.cpp"
9
#include "ada.h"
10
11
// ============================================================
12
// Fuzzer for IP address serializers, fast IPv4 parser, and
13
// percent encode/decode utilities.
14
//
15
// These code paths are exercised indirectly by parse.cc but
16
// never directly. Targeting them independently lets the fuzzer
17
// discover edge cases in the serialization and encoding layers
18
// without depending on a valid URL to reach them.
19
// ============================================================
20
21
2.87k
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
22
2.87k
  FuzzedDataProvider fdp(data, size);
23
24
  // ===== IPv4 Serialization =====
25
  // ipv4() takes a uint64_t (valid addresses are 0..0xFFFFFFFF, but the
26
  // function accepts any value – out-of-range inputs should still be safe).
27
2.87k
  uint64_t ipv4_addr = fdp.ConsumeIntegral<uint64_t>();
28
2.87k
  std::string ipv4_str = ada::serializers::ipv4(ipv4_addr);
29
2.87k
  volatile size_t ipv4_len = ipv4_str.size();
30
2.87k
  (void)ipv4_len;
31
32
  // ===== IPv6 Serialization =====
33
2.87k
  std::array<uint16_t, 8> ipv6_addr{};
34
23.0k
  for (auto& piece : ipv6_addr) {
35
23.0k
    piece = fdp.ConsumeIntegral<uint16_t>();
36
23.0k
  }
37
38
  // find_longest_sequence_of_ipv6_pieces: basic invariants must hold.
39
2.87k
  size_t compress = 0, compress_length = 0;
40
2.87k
  ada::serializers::find_longest_sequence_of_ipv6_pieces(ipv6_addr, compress,
41
2.87k
                                                         compress_length);
42
  // The longest run cannot exceed 8 pieces.
43
2.87k
  assert(compress_length <= 8);
44
  // If a run was found (length > 0) its start index must be in-bounds.
45
2.87k
  if (compress_length > 0) {
46
505
    assert(compress < 8);
47
505
    assert(compress + compress_length <= 8);
48
505
  }
49
50
2.87k
  std::string ipv6_str = ada::serializers::ipv6(ipv6_addr);
51
2.87k
  volatile size_t ipv6_len = ipv6_str.size();
52
2.87k
  (void)ipv6_len;
53
54
  // Serialized IPv6 must always be a non-empty string.
55
2.87k
  assert(!ipv6_str.empty());
56
57
  // ===== Fast IPv4 Parser =====
58
  // try_parse_ipv4_fast() should agree with the full parsing pipeline on its
59
  // output: if it succeeds (result <= 0xFFFFFFFF), re-serializing with
60
  // ipv4() and parsing again must yield the same address value.
61
2.87k
  {
62
2.87k
    std::string ip_candidate = fdp.ConsumeRandomLengthString(32);
63
2.87k
    uint64_t fast_result = ada::checkers::try_parse_ipv4_fast(ip_candidate);
64
2.87k
    if (fast_result <= 0xFFFFFFFF) {
65
      // Serialize the parsed address and re-parse.
66
38
      std::string canonical = ada::serializers::ipv4(fast_result);
67
38
      uint64_t recheck = ada::checkers::try_parse_ipv4_fast(canonical);
68
38
      if (recheck != fast_result) {
69
0
        printf(
70
0
            "try_parse_ipv4_fast round-trip failure:\n"
71
0
            "  input='%s' result=%llu canonical='%s' recheck=%llu\n",
72
0
            ip_candidate.c_str(), (unsigned long long)fast_result,
73
0
            canonical.c_str(), (unsigned long long)recheck);
74
0
        abort();
75
0
      }
76
38
    }
77
2.87k
  }
78
79
  // ===== Percent Encode / Decode =====
80
2.87k
  {
81
2.87k
    std::string source = fdp.ConsumeRandomLengthString(128);
82
83
    // Exercise all six standard character sets used by the URL parser.
84
2.87k
    const uint8_t* sets[] = {
85
2.87k
        ada::character_sets::C0_CONTROL_PERCENT_ENCODE,
86
2.87k
        ada::character_sets::PATH_PERCENT_ENCODE,
87
2.87k
        ada::character_sets::QUERY_PERCENT_ENCODE,
88
2.87k
        ada::character_sets::FRAGMENT_PERCENT_ENCODE,
89
2.87k
        ada::character_sets::USERINFO_PERCENT_ENCODE,
90
2.87k
        ada::character_sets::SPECIAL_QUERY_PERCENT_ENCODE,
91
2.87k
    };
92
93
17.2k
    for (const uint8_t* charset : sets) {
94
      // Two-argument percent_encode: returns the encoded string.
95
17.2k
      std::string encoded = ada::unicode::percent_encode(source, charset);
96
17.2k
      volatile size_t enc_len = encoded.size();
97
17.2k
      (void)enc_len;
98
99
      // Encoded output must be at least as long as the input (each byte
100
      // either stays the same or expands to %XX – three bytes).
101
17.2k
      assert(encoded.size() >= source.size());
102
103
      // Three-argument percent_encode: starts encoding from a given index.
104
17.2k
      size_t start_idx = fdp.ConsumeIntegralInRange<size_t>(0, source.size());
105
17.2k
      std::string encoded_from =
106
17.2k
          ada::unicode::percent_encode(source, charset, start_idx);
107
17.2k
      volatile size_t enf_len = encoded_from.size();
108
17.2k
      (void)enf_len;
109
110
      // Template form: percent_encode<false> (replace).
111
17.2k
      {
112
17.2k
        std::string out;
113
17.2k
        bool changed =
114
17.2k
            ada::unicode::percent_encode<false>(source, charset, out);
115
17.2k
        volatile bool c = changed;
116
17.2k
        (void)c;
117
        // When encoding was needed 'out' holds the encoded string; when not,
118
        // 'out' is unchanged (empty). If changed, 'out' must not be shorter.
119
17.2k
        if (changed) {
120
5.49k
          assert(out.size() >= source.size());
121
5.49k
        }
122
17.2k
      }
123
124
      // Template form: percent_encode<true> (append).
125
17.2k
      {
126
17.2k
        std::string out = "prefix_";
127
17.2k
        size_t prefix_len = out.size();
128
17.2k
        ada::unicode::percent_encode<true>(source, charset, out);
129
        // Append mode: 'out' must grow by at least source.size() bytes if
130
        // encoding was needed, or stay unchanged if it wasn't.
131
17.2k
        assert(out.size() >= prefix_len);
132
17.2k
      }
133
17.2k
    }
134
135
    // Percent decode: feed raw fuzz input (may contain invalid sequences).
136
2.87k
    {
137
2.87k
      size_t pct_pos = source.find('%');
138
2.87k
      if (pct_pos != std::string::npos) {
139
115
        std::string decoded = ada::unicode::percent_decode(source, pct_pos);
140
        // Decoded output can't be longer than the input.
141
115
        assert(decoded.size() <= source.size());
142
115
        volatile size_t dec_len = decoded.size();
143
115
        (void)dec_len;
144
115
      }
145
2.87k
    }
146
147
    // Round-trip: encode with PATH set, then decode the result; the decoded
148
    // form must equal the original (encoding then decoding is an identity).
149
2.87k
    {
150
2.87k
      std::string encoded = ada::unicode::percent_encode(
151
2.87k
          source, ada::character_sets::PATH_PERCENT_ENCODE);
152
2.87k
      size_t pct_pos = encoded.find('%');
153
2.87k
      if (pct_pos != std::string::npos) {
154
917
        std::string decoded = ada::unicode::percent_decode(encoded, pct_pos);
155
917
        if (decoded != source) {
156
0
          printf(
157
0
              "percent_encode/decode round-trip failure!\n"
158
0
              "  source='%s'\n  encoded='%s'\n  decoded='%s'\n",
159
0
              source.c_str(), encoded.c_str(), decoded.c_str());
160
0
          abort();
161
0
        }
162
1.95k
      } else {
163
        // No encoding was needed; the output should equal the input.
164
1.95k
        assert(encoded == source);
165
1.95k
      }
166
2.87k
    }
167
168
    // percent_encode_index: the returned index must be within [0, size].
169
2.87k
    {
170
2.87k
      size_t idx = ada::unicode::percent_encode_index(
171
2.87k
          source, ada::character_sets::PATH_PERCENT_ENCODE);
172
2.87k
      assert(idx <= source.size());
173
2.87k
    }
174
2.87k
  }
175
176
  // ===== Checker and Unicode Utility Functions =====
177
  // These are internal helpers used throughout the parser. Fuzzing them
178
  // directly (rather than only through the URL parsing pipeline) ensures
179
  // every edge case is reachable without a valid URL structure.
180
2.87k
  {
181
2.87k
    std::string util_input = fdp.ConsumeRandomLengthString(128);
182
183
    // has_tabs_or_newline: any string is valid input.
184
2.87k
    volatile bool has_tn = ada::unicode::has_tabs_or_newline(util_input);
185
2.87k
    (void)has_tn;
186
187
    // is_ipv4: must not crash on arbitrary input. Cross-check against URL
188
    // parsing: if is_ipv4 reports true, embedding the string as a hostname
189
    // in an http:// URL must succeed (it must be a parseable IPv4 address).
190
2.87k
    volatile bool is_v4 = ada::checkers::is_ipv4(util_input);
191
2.87k
    (void)is_v4;
192
2.87k
    if (is_v4) {
193
2.11k
      std::string ipv4_url = "http://" + util_input + "/";
194
2.11k
      auto parsed = ada::parse<ada::url_aggregator>(ipv4_url);
195
      // is_ipv4 reports true only for strings that look like IPv4 addresses;
196
      // the full parser may still reject them (e.g. out-of-range octets), but
197
      // if it accepts them the host type must be IPv4.
198
2.11k
      if (parsed) {
199
797
        volatile bool v = parsed->validate();
200
797
        (void)v;
201
797
      }
202
2.11k
    }
203
204
    // path_signature: returns a bitmask; must not crash.
205
2.87k
    volatile uint8_t sig = ada::checkers::path_signature(util_input);
206
2.87k
    (void)sig;
207
208
    // is_windows_drive_letter: must not crash on short or long inputs.
209
2.87k
    volatile bool is_wdl = ada::checkers::is_windows_drive_letter(util_input);
210
2.87k
    (void)is_wdl;
211
212
    // is_normalized_windows_drive_letter
213
2.87k
    volatile bool is_nwdl =
214
2.87k
        ada::checkers::is_normalized_windows_drive_letter(util_input);
215
2.87k
    (void)is_nwdl;
216
217
    // Consistency: a normalised Windows drive letter is a subset of
218
    // Windows drive letters.
219
2.87k
    if (is_nwdl && !is_wdl) {
220
0
      printf(
221
0
          "is_normalized_windows_drive_letter implies is_windows_drive_letter"
222
0
          " but got inconsistent results for '%s'\n",
223
0
          util_input.c_str());
224
0
      abort();
225
0
    }
226
227
    // to_lower_ascii: works in-place; must not crash.
228
2.87k
    {
229
2.87k
      std::string lower_copy = util_input;
230
2.87k
      volatile bool all_ascii =
231
2.87k
          ada::unicode::to_lower_ascii(lower_copy.data(), lower_copy.size());
232
2.87k
      (void)all_ascii;
233
      // The result should be at most as long as the input.
234
2.87k
      assert(lower_copy.size() == util_input.size());
235
2.87k
    }
236
237
    // contains_forbidden_domain_code_point: must not crash.
238
2.87k
    volatile bool has_forbidden =
239
2.87k
        ada::unicode::contains_forbidden_domain_code_point(util_input.data(),
240
2.87k
                                                           util_input.size());
241
2.87k
    (void)has_forbidden;
242
243
    // contains_forbidden_domain_code_point_or_upper: must not crash.
244
2.87k
    volatile uint8_t forbidden_or_upper =
245
2.87k
        ada::unicode::contains_forbidden_domain_code_point_or_upper(
246
2.87k
            util_input.data(), util_input.size());
247
2.87k
    (void)forbidden_or_upper;
248
249
    // verify_dns_length: must not crash; also check consistency with
250
    // to_ascii output.
251
2.87k
    volatile bool dns_ok = ada::checkers::verify_dns_length(util_input);
252
2.87k
    (void)dns_ok;
253
2.87k
  }
254
255
  // ===== Integration: embed serialized addresses into real URLs =====
256
  // This exercises the full parsing pipeline with our synthetic addresses
257
  // and verifies that serialization output is always round-trip safe.
258
0
  {
259
    // Only valid IPv4 address range (0..0xFFFFFFFF) should embed cleanly.
260
2.87k
    if (ipv4_addr <= 0xFFFFFFFF) {
261
146
      std::string url_str = "http://" + ipv4_str + "/path?q=1";
262
146
      auto parsed = ada::parse<ada::url_aggregator>(url_str);
263
146
      if (parsed) {
264
146
        volatile bool v = parsed->validate();
265
146
        (void)v;
266
        // The URL's hostname must be the canonical dotted-decimal address.
267
146
        std::string host = std::string(parsed->get_hostname());
268
        // host == ipv4_str  (not asserted here because the URL parser may
269
        // normalise the address differently for non-standard values, but it
270
        // must always successfully parse our serialized form).
271
146
        (void)host;
272
146
      }
273
146
    }
274
275
    // IPv6: wrap in brackets.
276
2.87k
    std::string ipv6_url_str = "http://[" + ipv6_str + "]/path";
277
2.87k
    auto parsed_ipv6 = ada::parse<ada::url_aggregator>(ipv6_url_str);
278
2.87k
    if (parsed_ipv6) {
279
0
      volatile bool v = parsed_ipv6->validate();
280
0
      (void)v;
281
      // Re-parse the href – must be idempotent.
282
0
      std::string href = std::string(parsed_ipv6->get_href());
283
0
      auto reparsed = ada::parse<ada::url_aggregator>(href);
284
0
      if (!reparsed) {
285
0
        printf("IPv6 URL re-parse failure: '%s'\n", href.c_str());
286
0
        abort();
287
0
      }
288
0
      if (std::string(reparsed->get_href()) != href) {
289
0
        printf("IPv6 URL re-parse href mismatch: '%s' vs '%s'\n", href.c_str(),
290
0
               std::string(reparsed->get_href()).c_str());
291
0
        abort();
292
0
      }
293
0
    }
294
2.87k
  }
295
296
2.87k
  return 0;
297
2.87k
}