Coverage Report

Created: 2026-08-13 07:21

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
3.54k
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
22
3.54k
  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
3.54k
  uint64_t ipv4_addr = fdp.ConsumeIntegral<uint64_t>();
28
3.54k
  std::string ipv4_str = ada::serializers::ipv4(ipv4_addr);
29
3.54k
  volatile size_t ipv4_len = ipv4_str.size();
30
3.54k
  (void)ipv4_len;
31
32
  // ===== IPv6 Serialization =====
33
3.54k
  std::array<uint16_t, 8> ipv6_addr{};
34
28.3k
  for (auto& piece : ipv6_addr) {
35
28.3k
    piece = fdp.ConsumeIntegral<uint16_t>();
36
28.3k
  }
37
38
  // find_longest_sequence_of_ipv6_pieces: basic invariants must hold.
39
3.54k
  size_t compress = 0, compress_length = 0;
40
3.54k
  ada::serializers::find_longest_sequence_of_ipv6_pieces(ipv6_addr, compress,
41
3.54k
                                                         compress_length);
42
  // The longest run cannot exceed 8 pieces.
43
3.54k
  assert(compress_length <= 8);
44
  // If a run was found (length > 0) its start index must be in-bounds.
45
3.54k
  if (compress_length > 0) {
46
590
    assert(compress < 8);
47
590
    assert(compress + compress_length <= 8);
48
590
  }
49
50
3.54k
  std::string ipv6_str = ada::serializers::ipv6(ipv6_addr);
51
3.54k
  volatile size_t ipv6_len = ipv6_str.size();
52
3.54k
  (void)ipv6_len;
53
54
  // Serialized IPv6 must always be a non-empty string.
55
3.54k
  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
3.54k
  {
62
3.54k
    std::string ip_candidate = fdp.ConsumeRandomLengthString(32);
63
3.54k
    uint64_t fast_result = ada::checkers::try_parse_ipv4_fast(ip_candidate);
64
3.54k
    if (fast_result <= 0xFFFFFFFF) {
65
      // Serialize the parsed address and re-parse.
66
49
      std::string canonical = ada::serializers::ipv4(fast_result);
67
49
      uint64_t recheck = ada::checkers::try_parse_ipv4_fast(canonical);
68
49
      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
49
    }
77
3.54k
  }
78
79
  // ===== Percent Encode / Decode =====
80
3.54k
  {
81
3.54k
    std::string source = fdp.ConsumeRandomLengthString(128);
82
83
    // Exercise all six standard character sets used by the URL parser.
84
3.54k
    const uint8_t* sets[] = {
85
3.54k
        ada::character_sets::C0_CONTROL_PERCENT_ENCODE,
86
3.54k
        ada::character_sets::PATH_PERCENT_ENCODE,
87
3.54k
        ada::character_sets::QUERY_PERCENT_ENCODE,
88
3.54k
        ada::character_sets::FRAGMENT_PERCENT_ENCODE,
89
3.54k
        ada::character_sets::USERINFO_PERCENT_ENCODE,
90
3.54k
        ada::character_sets::SPECIAL_QUERY_PERCENT_ENCODE,
91
3.54k
    };
92
93
21.2k
    for (const uint8_t* charset : sets) {
94
      // Two-argument percent_encode: returns the encoded string.
95
21.2k
      std::string encoded = ada::unicode::percent_encode(source, charset);
96
21.2k
      volatile size_t enc_len = encoded.size();
97
21.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
21.2k
      assert(encoded.size() >= source.size());
102
103
      // Three-argument percent_encode: starts encoding from a given index.
104
21.2k
      size_t start_idx = fdp.ConsumeIntegralInRange<size_t>(0, source.size());
105
21.2k
      std::string encoded_from =
106
21.2k
          ada::unicode::percent_encode(source, charset, start_idx);
107
21.2k
      volatile size_t enf_len = encoded_from.size();
108
21.2k
      (void)enf_len;
109
110
      // Template form: percent_encode<false> (replace).
111
21.2k
      {
112
21.2k
        std::string out;
113
21.2k
        bool changed =
114
21.2k
            ada::unicode::percent_encode<false>(source, charset, out);
115
21.2k
        volatile bool c = changed;
116
21.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
21.2k
        if (changed) {
120
5.42k
          assert(out.size() >= source.size());
121
5.42k
        }
122
21.2k
      }
123
124
      // Template form: percent_encode<true> (append).
125
21.2k
      {
126
21.2k
        std::string out = "prefix_";
127
21.2k
        size_t prefix_len = out.size();
128
21.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
21.2k
        assert(out.size() >= prefix_len);
132
21.2k
      }
133
21.2k
    }
134
135
    // Percent decode: feed raw fuzz input (may contain invalid sequences).
136
3.54k
    {
137
3.54k
      size_t pct_pos = source.find('%');
138
3.54k
      if (pct_pos != std::string::npos) {
139
125
        std::string decoded = ada::unicode::percent_decode(source, pct_pos);
140
        // Decoded output can't be longer than the input.
141
125
        assert(decoded.size() <= source.size());
142
125
        volatile size_t dec_len = decoded.size();
143
125
        (void)dec_len;
144
125
      }
145
3.54k
    }
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
3.54k
    {
150
3.54k
      std::string encoded = ada::unicode::percent_encode(
151
3.54k
          source, ada::character_sets::PATH_PERCENT_ENCODE);
152
3.54k
      size_t pct_pos = encoded.find('%');
153
3.54k
      if (pct_pos != std::string::npos) {
154
916
        std::string decoded = ada::unicode::percent_decode(encoded, pct_pos);
155
916
        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
2.62k
      } else {
163
        // No encoding was needed; the output should equal the input.
164
2.62k
        assert(encoded == source);
165
2.62k
      }
166
3.54k
    }
167
168
    // percent_encode_index: the returned index must be within [0, size].
169
3.54k
    {
170
3.54k
      size_t idx = ada::unicode::percent_encode_index(
171
3.54k
          source, ada::character_sets::PATH_PERCENT_ENCODE);
172
3.54k
      assert(idx <= source.size());
173
3.54k
    }
174
3.54k
  }
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
3.54k
  {
181
3.54k
    std::string util_input = fdp.ConsumeRandomLengthString(128);
182
183
    // has_tabs_or_newline: any string is valid input.
184
3.54k
    volatile bool has_tn = ada::unicode::has_tabs_or_newline(util_input);
185
3.54k
    (void)has_tn;
186
187
    // is_ipv4 assumes a non-empty, lower-cased input, so honor that
188
    // precondition here. Cross-check against URL parsing: if is_ipv4 reports
189
    // true, embedding the string as a hostname in an http:// URL must succeed
190
    // (it must be a parseable IPv4 address).
191
3.54k
    if (!util_input.empty()) {
192
2.87k
      volatile bool is_v4 = ada::checkers::is_ipv4(util_input);
193
2.87k
      (void)is_v4;
194
2.87k
      if (is_v4) {
195
2.68k
        std::string ipv4_url = "http://" + util_input + "/";
196
2.68k
        auto parsed = ada::parse<ada::url_aggregator>(ipv4_url);
197
        // is_ipv4 reports true only for strings that look like IPv4 addresses;
198
        // the full parser may still reject them (e.g. out-of-range octets), but
199
        // if it accepts them the host type must be IPv4.
200
2.68k
        if (parsed) {
201
1.00k
          volatile bool v = parsed->validate();
202
1.00k
          (void)v;
203
1.00k
        }
204
2.68k
      }
205
2.87k
    }
206
207
    // path_signature: returns a bitmask; must not crash.
208
3.54k
    volatile uint8_t sig = ada::checkers::path_signature(util_input);
209
3.54k
    (void)sig;
210
211
    // is_windows_drive_letter: must not crash on short or long inputs.
212
3.54k
    volatile bool is_wdl = ada::checkers::is_windows_drive_letter(util_input);
213
3.54k
    (void)is_wdl;
214
215
    // is_normalized_windows_drive_letter
216
3.54k
    volatile bool is_nwdl =
217
3.54k
        ada::checkers::is_normalized_windows_drive_letter(util_input);
218
3.54k
    (void)is_nwdl;
219
220
    // Consistency: a normalised Windows drive letter is a subset of
221
    // Windows drive letters.
222
3.54k
    if (is_nwdl && !is_wdl) {
223
0
      printf(
224
0
          "is_normalized_windows_drive_letter implies is_windows_drive_letter"
225
0
          " but got inconsistent results for '%s'\n",
226
0
          util_input.c_str());
227
0
      abort();
228
0
    }
229
230
    // to_lower_ascii: works in-place; must not crash.
231
3.54k
    {
232
3.54k
      std::string lower_copy = util_input;
233
3.54k
      volatile bool all_ascii =
234
3.54k
          ada::unicode::to_lower_ascii(lower_copy.data(), lower_copy.size());
235
3.54k
      (void)all_ascii;
236
      // The result should be at most as long as the input.
237
3.54k
      assert(lower_copy.size() == util_input.size());
238
3.54k
    }
239
240
    // contains_forbidden_domain_code_point: must not crash.
241
3.54k
    volatile bool has_forbidden =
242
3.54k
        ada::unicode::contains_forbidden_domain_code_point(util_input.data(),
243
3.54k
                                                           util_input.size());
244
3.54k
    (void)has_forbidden;
245
246
    // contains_forbidden_domain_code_point_or_upper: must not crash.
247
3.54k
    volatile uint8_t forbidden_or_upper =
248
3.54k
        ada::unicode::contains_forbidden_domain_code_point_or_upper(
249
3.54k
            util_input.data(), util_input.size());
250
3.54k
    (void)forbidden_or_upper;
251
252
    // verify_dns_length: must not crash; also check consistency with
253
    // to_ascii output.
254
3.54k
    volatile bool dns_ok = ada::checkers::verify_dns_length(util_input);
255
3.54k
    (void)dns_ok;
256
3.54k
  }
257
258
  // ===== Integration: embed serialized addresses into real URLs =====
259
  // This exercises the full parsing pipeline with our synthetic addresses
260
  // and verifies that serialization output is always round-trip safe.
261
0
  {
262
    // Only valid IPv4 address range (0..0xFFFFFFFF) should embed cleanly.
263
3.54k
    if (ipv4_addr <= 0xFFFFFFFF) {
264
186
      std::string url_str = "http://" + ipv4_str + "/path?q=1";
265
186
      auto parsed = ada::parse<ada::url_aggregator>(url_str);
266
186
      if (parsed) {
267
186
        volatile bool v = parsed->validate();
268
186
        (void)v;
269
        // The URL's hostname must be the canonical dotted-decimal address.
270
186
        std::string host = std::string(parsed->get_hostname());
271
        // host == ipv4_str  (not asserted here because the URL parser may
272
        // normalise the address differently for non-standard values, but it
273
        // must always successfully parse our serialized form).
274
186
        (void)host;
275
186
      }
276
186
    }
277
278
    // IPv6: wrap in brackets.
279
3.54k
    std::string ipv6_url_str = "http://[" + ipv6_str + "]/path";
280
3.54k
    auto parsed_ipv6 = ada::parse<ada::url_aggregator>(ipv6_url_str);
281
3.54k
    if (parsed_ipv6) {
282
0
      volatile bool v = parsed_ipv6->validate();
283
0
      (void)v;
284
      // Re-parse the href – must be idempotent.
285
0
      std::string href = std::string(parsed_ipv6->get_href());
286
0
      auto reparsed = ada::parse<ada::url_aggregator>(href);
287
0
      if (!reparsed) {
288
0
        printf("IPv6 URL re-parse failure: '%s'\n", href.c_str());
289
0
        abort();
290
0
      }
291
0
      if (std::string(reparsed->get_href()) != href) {
292
0
        printf("IPv6 URL re-parse href mismatch: '%s' vs '%s'\n", href.c_str(),
293
0
               std::string(reparsed->get_href()).c_str());
294
0
        abort();
295
0
      }
296
0
    }
297
3.54k
  }
298
299
3.54k
  return 0;
300
3.54k
}