Coverage Report

Created: 2026-09-03 06:31

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