Coverage Report

Created: 2026-07-16 06:37

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/ada-url/fuzz/idna.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
4.42k
extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
10
4.42k
  FuzzedDataProvider fdp(data, size);
11
4.42k
  std::string source = fdp.ConsumeRandomLengthString(256);
12
4.42k
  std::string source2 = fdp.ConsumeRandomLengthString(64);
13
14
  /**
15
   * High-level IDNA API
16
   */
17
4.42k
  std::string ascii_result = ada::idna::to_ascii(source);
18
4.42k
  std::string unicode_result = ada::idna::to_unicode(source);
19
20
  // Avoid dead-code elimination
21
4.42k
  volatile size_t length = 0;
22
4.42k
  length += ascii_result.size();
23
4.42k
  length += unicode_result.size();
24
25
  /**
26
   * Round-trip property: to_unicode(to_ascii(x)) should not crash.
27
   * We don't assert equality because IDNA may normalize/reject inputs.
28
   */
29
4.42k
  if (!ascii_result.empty()) {
30
2.71k
    std::string roundtrip = ada::idna::to_unicode(ascii_result);
31
2.71k
    length += roundtrip.size();
32
2.71k
  }
33
34
  /**
35
   * Punycode functions
36
   */
37
4.42k
  {
38
4.42k
    std::u32string utf32_out;
39
    // punycode_to_utf32: source can be any string (it's a punycode label)
40
4.42k
    bool punycode_ok = ada::idna::punycode_to_utf32(source, utf32_out);
41
4.42k
    length += utf32_out.size();
42
43
    // verify_punycode: checks if source is valid punycode
44
4.42k
    volatile bool is_valid_punycode = ada::idna::verify_punycode(source);
45
4.42k
    (void)is_valid_punycode;
46
47
    // utf32_to_punycode: round-trip if punycode_to_utf32 succeeded
48
4.42k
    if (punycode_ok && !utf32_out.empty()) {
49
802
      std::string punycode_back;
50
802
      volatile bool encode_ok =
51
802
          ada::idna::utf32_to_punycode(utf32_out, punycode_back);
52
802
      length += punycode_back.size();
53
802
      (void)encode_ok;
54
802
    }
55
4.42k
  }
56
57
  /**
58
   * Unicode transcoding
59
   */
60
4.42k
  {
61
    // UTF-8 to UTF-32 conversion
62
4.42k
    size_t utf32_len =
63
4.42k
        ada::idna::utf32_length_from_utf8(source.data(), source.size());
64
4.42k
    if (utf32_len > 0 && utf32_len < 1024) {
65
3.87k
      std::vector<char32_t> utf32_buf(utf32_len + 1, 0);
66
3.87k
      size_t actual = ada::idna::utf8_to_utf32(source.data(), source.size(),
67
3.87k
                                               utf32_buf.data());
68
3.87k
      length += actual;
69
70
      // UTF-32 to UTF-8 round-trip
71
3.87k
      if (actual > 0) {
72
3.67k
        size_t utf8_len =
73
3.67k
            ada::idna::utf8_length_from_utf32(utf32_buf.data(), actual);
74
3.67k
        if (utf8_len > 0 && utf8_len < 4096) {
75
3.67k
          std::string utf8_back(utf8_len, '\0');
76
3.67k
          size_t written = ada::idna::utf32_to_utf8(utf32_buf.data(), actual,
77
3.67k
                                                    utf8_back.data());
78
3.67k
          length += written;
79
3.67k
        }
80
3.67k
      }
81
3.87k
    }
82
4.42k
  }
83
84
  /**
85
   * IDNA label validation
86
   */
87
4.42k
  {
88
    // is_label_valid requires a UTF-32 string
89
4.42k
    std::u32string utf32_label;
90
4.42k
    bool ok = ada::idna::punycode_to_utf32(source2, utf32_label);
91
4.42k
    if (ok && !utf32_label.empty()) {
92
177
      volatile bool label_valid = ada::idna::is_label_valid(utf32_label);
93
177
      (void)label_valid;
94
177
    }
95
96
    // Also test is_label_valid with direct ASCII-to-UTF32 conversion
97
4.42k
    std::u32string ascii_label(source2.begin(), source2.end());
98
4.42k
    if (!ascii_label.empty()) {
99
340
      volatile bool ascii_label_valid = ada::idna::is_label_valid(ascii_label);
100
340
      (void)ascii_label_valid;
101
340
    }
102
4.42k
  }
103
104
  /**
105
   * IDNA mapping
106
   */
107
4.42k
  {
108
    // ASCII mapping: just lowercases ASCII characters
109
4.42k
    std::string ascii_copy = source;
110
4.42k
    ada::idna::ascii_map(ascii_copy.data(), ascii_copy.size());
111
4.42k
    length += ascii_copy.size();
112
113
    // Unicode mapping: maps UTF-32 characters according to IDNA
114
4.42k
    size_t utf32_len =
115
4.42k
        ada::idna::utf32_length_from_utf8(source.data(), source.size());
116
4.42k
    if (utf32_len > 0 && utf32_len < 256) {
117
3.84k
      std::u32string utf32_input(utf32_len, 0);
118
3.84k
      size_t actual = ada::idna::utf8_to_utf32(source.data(), source.size(),
119
3.84k
                                               utf32_input.data());
120
3.84k
      if (actual > 0) {
121
3.65k
        utf32_input.resize(actual);
122
3.65k
        std::u32string mapped = ada::idna::map(utf32_input);
123
3.65k
        length += mapped.size();
124
3.65k
      }
125
3.84k
    }
126
4.42k
  }
127
128
  /**
129
   * Domain code point validation
130
   */
131
4.42k
  {
132
4.42k
    volatile bool has_forbidden =
133
4.42k
        ada::idna::contains_forbidden_domain_code_point(source);
134
4.42k
    (void)has_forbidden;
135
136
    // is_ascii checks
137
4.42k
    volatile bool is_ascii_str =
138
4.42k
        ada::idna::is_ascii(std::string_view(source.data(), source.size()));
139
4.42k
    (void)is_ascii_str;
140
4.42k
  }
141
142
  /**
143
   * Normalization
144
   */
145
4.42k
  {
146
4.42k
    size_t utf32_len =
147
4.42k
        ada::idna::utf32_length_from_utf8(source.data(), source.size());
148
4.42k
    if (utf32_len > 0 && utf32_len < 256) {
149
3.84k
      std::u32string utf32_input(utf32_len, 0);
150
3.84k
      size_t actual = ada::idna::utf8_to_utf32(source.data(), source.size(),
151
3.84k
                                               utf32_input.data());
152
3.84k
      if (actual > 0) {
153
3.65k
        utf32_input.resize(actual);
154
3.65k
        ada::idna::normalize(utf32_input);
155
3.65k
        length += utf32_input.size();
156
3.65k
      }
157
3.84k
    }
158
4.42k
  }
159
160
  /**
161
   * IDNA stability property.
162
   *
163
   * Applying to_ascii twice must be idempotent: if to_ascii(x) produces a
164
   * non-empty result, then to_ascii(to_ascii(x)) must equal to_ascii(x).
165
   * A correctly normalised ACE label is already its own fixed point.
166
   *
167
   * We allow the second call to return an empty string only if the first
168
   * result was itself not a valid IDNA domain (some implementations return
169
   * empty on failure). If the first result is non-empty and looks like a
170
   * valid domain the second application must match.
171
   */
172
4.42k
  {
173
4.42k
    if (!ascii_result.empty()) {
174
2.71k
      std::string ascii_result2 = ada::idna::to_ascii(ascii_result);
175
2.71k
      if (!ascii_result2.empty() && ascii_result2 != ascii_result) {
176
0
        printf(
177
0
            "IDNA to_ascii not idempotent!\n"
178
0
            "  input:   %s\n  first:   %s\n  second:  %s\n",
179
0
            source.c_str(), ascii_result.c_str(), ascii_result2.c_str());
180
0
        abort();
181
0
      }
182
2.71k
    }
183
4.42k
  }
184
185
  /**
186
   * to_unicode stability.
187
   *
188
   * Applying to_unicode twice should also be idempotent: once a domain is in
189
   * its Unicode presentation form, converting again should give the same
190
   * result.
191
   */
192
4.42k
  {
193
4.42k
    if (!unicode_result.empty()) {
194
3.89k
      std::string unicode_result2 = ada::idna::to_unicode(unicode_result);
195
3.89k
      if (!unicode_result2.empty() && unicode_result2 != unicode_result) {
196
0
        printf(
197
0
            "IDNA to_unicode not idempotent!\n"
198
0
            "  input:   %s\n  first:   %s\n  second:  %s\n",
199
0
            source.c_str(), unicode_result.c_str(), unicode_result2.c_str());
200
0
        abort();
201
0
      }
202
3.89k
    }
203
4.42k
  }
204
205
  /**
206
   * Long domain names near the DNS length limit (253 characters).
207
   *
208
   * The IDNA and DNS-length checking code has special handling for domains
209
   * close to or exceeding 253/255 characters and 63-character labels. Feed
210
   * the fuzzer inputs of a controlled length to maximise coverage of those
211
   * boundary checks.
212
   */
213
4.42k
  {
214
4.42k
    std::string long_domain = fdp.ConsumeRandomLengthString(270);
215
4.42k
    std::string long_ascii = ada::idna::to_ascii(long_domain);
216
4.42k
    length += long_ascii.size();
217
218
    // verify_dns_length on the long input (already called for `source` above,
219
    // but we want to exercise the boundary cases separately).
220
4.42k
    volatile bool long_dns_ok = ada::checkers::verify_dns_length(long_domain);
221
4.42k
    (void)long_dns_ok;
222
223
4.42k
    if (!long_ascii.empty()) {
224
195
      volatile bool long_ascii_dns_ok =
225
195
          ada::checkers::verify_dns_length(long_ascii);
226
195
      (void)long_ascii_dns_ok;
227
195
    }
228
4.42k
  }
229
230
  /**
231
   * Punycode round-trip on arbitrary binary blobs.
232
   *
233
   * Feed random bytes directly into punycode_to_utf32, then if that succeeds
234
   * encode the result back with utf32_to_punycode and verify the round-trip.
235
   */
236
4.42k
  {
237
4.42k
    std::string blob = fdp.ConsumeRandomLengthString(128);
238
4.42k
    std::u32string decoded;
239
4.42k
    bool ok = ada::idna::punycode_to_utf32(blob, decoded);
240
4.42k
    if (ok && !decoded.empty()) {
241
41
      std::string reencoded;
242
41
      bool enc_ok = ada::idna::utf32_to_punycode(decoded, reencoded);
243
41
      (void)enc_ok;
244
41
      length += reencoded.size();
245
246
      // Re-decode the re-encoded form; it must match the first decoded form.
247
41
      if (enc_ok && !reencoded.empty()) {
248
38
        std::u32string redecoded;
249
38
        bool redec_ok = ada::idna::punycode_to_utf32(reencoded, redecoded);
250
38
        if (redec_ok && redecoded != decoded) {
251
0
          printf("Punycode round-trip mismatch!\n");
252
0
          abort();
253
0
        }
254
38
      }
255
41
    }
256
4.42k
  }
257
258
4.42k
  return 0;
259
4.42k
}