Coverage Report

Created: 2026-10-15 06:25

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/curl_fuzzer/proto_fuzzer/h2_plan.cc
Line
Count
Source
1
/*
2
 * Copyright (C) Max Dymond, <cmeister2@gmail.com>, et al.
3
 *
4
 * SPDX-License-Identifier: curl
5
 */
6
7
/// @file
8
/// @brief HTTP/2 structured-action serialization and client frame tracking.
9
10
#include "proto_fuzzer/h2_plan.h"
11
12
#include <nghttp2/nghttp2.h>
13
14
#include <algorithm>
15
#include <array>
16
#include <cstddef>
17
#include <cstdint>
18
#include <string>
19
#include <string_view>
20
#include <utility>
21
#include <vector>
22
23
#include "proto_fuzzer/scenario_limits.h"
24
25
namespace proto_fuzzer {
26
27
namespace {
28
29
constexpr std::string_view kClientPreface = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
30
constexpr std::size_t kFrameHeaderSize = 9;
31
constexpr std::size_t kMaxFramePayload = 16384;
32
33
constexpr std::uint8_t kDataFrame = 0;
34
constexpr std::uint8_t kHeadersFrame = 1;
35
constexpr std::uint8_t kRstStreamFrame = 3;
36
constexpr std::uint8_t kSettingsFrame = 4;
37
constexpr std::uint8_t kPushPromiseFrame = 5;
38
constexpr std::uint8_t kPingFrame = 6;
39
constexpr std::uint8_t kGoawayFrame = 7;
40
constexpr std::uint8_t kWindowUpdateFrame = 8;
41
constexpr std::uint8_t kContinuationFrame = 9;
42
43
constexpr std::uint8_t kFlagAck = 0x1;
44
constexpr std::uint8_t kFlagEndStream = 0x1;
45
constexpr std::uint8_t kFlagEndHeaders = 0x4;
46
47
160k
void AppendUint32(std::string* output, std::uint32_t value) {
48
160k
  output->push_back(static_cast<char>((value >> 24U) & 0xffU));
49
160k
  output->push_back(static_cast<char>((value >> 16U) & 0xffU));
50
160k
  output->push_back(static_cast<char>((value >> 8U) & 0xffU));
51
160k
  output->push_back(static_cast<char>(value & 0xffU));
52
160k
}
53
54
385k
std::uint32_t ReadUint32(const char* input) {
55
385k
  const auto* bytes = reinterpret_cast<const unsigned char*>(input);
56
385k
  return (static_cast<std::uint32_t>(bytes[0]) << 24U) | (static_cast<std::uint32_t>(bytes[1]) << 16U) |
57
385k
         (static_cast<std::uint32_t>(bytes[2]) << 8U) | static_cast<std::uint32_t>(bytes[3]);
58
385k
}
59
60
void AppendFrame(std::string* output, std::uint32_t declared_length, std::uint8_t type, std::uint8_t flags,
61
125k
                 std::uint32_t stream_id, std::string_view payload) {
62
125k
  const std::uint32_t length = declared_length & 0x00ffffffU;
63
125k
  output->push_back(static_cast<char>((length >> 16U) & 0xffU));
64
125k
  output->push_back(static_cast<char>((length >> 8U) & 0xffU));
65
125k
  output->push_back(static_cast<char>(length & 0xffU));
66
125k
  output->push_back(static_cast<char>(type));
67
125k
  output->push_back(static_cast<char>(flags));
68
125k
  AppendUint32(output, stream_id & 0x7fffffffU);
69
125k
  output->append(payload.data(), payload.size());
70
125k
}
71
72
45.7k
std::uint32_t CanonicalStatus(std::uint32_t status) {
73
45.7k
  if (status >= 100U && status <= 599U) {
74
45.7k
    return status;
75
45.7k
  }
76
0
  return status == 0U ? 200U : 100U + (status % 500U);
77
45.7k
}
78
79
137k
std::string BoundedString(const std::string& input, std::size_t limit) { return input.substr(0, limit); }
80
81
62.3k
std::string CanonicalHeaderName(const std::string& input) {
82
62.3k
  std::string name = BoundedString(input, scenario_limits::kMaxHttp3HeaderNameBytes);
83
62.3k
  if (name.empty()) {
84
0
    return "x-fuzz";
85
0
  }
86
530k
  for (char& byte : name) {
87
530k
    const unsigned char value = static_cast<unsigned char>(byte);
88
530k
    const bool alpha = (value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z');
89
530k
    const bool digit = value >= '0' && value <= '9';
90
530k
    const bool punctuation = value == '!' || value == '#' || value == '$' || value == '%' || value == '&' ||
91
524k
                             value == '\'' || value == '*' || value == '+' || value == '-' || value == '.' ||
92
407k
                             value == '^' || value == '_' || value == '`' || value == '|' || value == '~';
93
530k
    if (value >= 'A' && value <= 'Z') {
94
0
      byte = static_cast<char>(value - 'A' + 'a');
95
530k
    } else if (!alpha && !digit && !punctuation) {
96
0
      byte = '-';
97
0
    }
98
530k
  }
99
62.3k
  return name;
100
62.3k
}
101
102
62.3k
std::string CanonicalHeaderValue(const std::string& input) {
103
62.3k
  std::string value = BoundedString(input, scenario_limits::kMaxMetadataBytes);
104
4.96M
  for (char& byte : value) {
105
4.96M
    const unsigned char character = static_cast<unsigned char>(byte);
106
4.96M
    if ((character < 0x20U && character != '\t') || character == 0x7fU) {
107
0
      byte = ' ';
108
0
    }
109
4.96M
  }
110
62.3k
  return value;
111
62.3k
}
112
113
}  // namespace
114
115
H2PlanDriver::H2PlanDriver()
116
36.2k
    : plan_(nullptr),
117
36.2k
      deflater_(nullptr),
118
36.2k
      next_action_(0),
119
36.2k
      remaining_yields_(0),
120
36.2k
      initial_settings_sent_(false),
121
36.2k
      client_settings_ack_sent_(false),
122
36.2k
      expect_client_preface_(true),
123
36.2k
      observed_client_settings_(0) {
124
36.2k
  (void)nghttp2_hd_deflate_new(&deflater_, 4096);
125
36.2k
}
126
127
36.2k
H2PlanDriver::~H2PlanDriver() { nghttp2_hd_deflate_del(deflater_); }
128
129
/// Reset HPACK and stream state for one connection.
130
/// @param plan Ordered H2 actions and initial peer settings to execute.
131
/// @param scheme Scheme used in generated push-request pseudo-headers.
132
/// @param authority Authority used in generated push-request pseudo-headers.
133
19.7k
void H2PlanDriver::Reset(const curl::fuzzer::proto::Http2Plan& plan, std::string scheme, std::string authority) {
134
19.7k
  plan_ = &plan;
135
19.7k
  next_action_ = 0;
136
19.7k
  remaining_yields_ = 0;
137
19.7k
  initial_settings_sent_ = false;
138
19.7k
  client_settings_ack_sent_ = false;
139
19.7k
  expect_client_preface_ = true;
140
19.7k
  observed_client_settings_ = 0;
141
19.7k
  client_bytes_.clear();
142
19.7k
  request_streams_.clear();
143
19.7k
  pushed_streams_.clear();
144
19.7k
  client_streams_.clear();
145
19.7k
  scheme_ = std::move(scheme);
146
19.7k
  authority_ = std::move(authority);
147
19.7k
  nghttp2_hd_deflate_del(deflater_);
148
19.7k
  deflater_ = nullptr;
149
19.7k
  (void)nghttp2_hd_deflate_new(&deflater_, 4096);
150
19.7k
}
151
152
/// Parse client frame headers after transport decoding.
153
/// @param data Contiguous bytes received from curl.
154
/// @param size Number of bytes available at `data`.
155
212k
void H2PlanDriver::ObserveIncomingData(const unsigned char* data, std::size_t size) {
156
212k
  if (data == nullptr || size == 0) {
157
0
    return;
158
0
  }
159
212k
  if (size > scenario_limits::kMaxHttp2ObservedBytes ||
160
212k
      client_bytes_.size() > scenario_limits::kMaxHttp2ObservedBytes - size) {
161
0
    client_bytes_.clear();
162
0
    expect_client_preface_ = false;
163
0
    return;
164
0
  }
165
212k
  client_bytes_.append(reinterpret_cast<const char*>(data), size);
166
212k
  ParseClientFrames();
167
212k
}
168
169
212k
void H2PlanDriver::ParseClientFrames() {
170
212k
  if (expect_client_preface_) {
171
34.2k
    if (client_bytes_.size() < kClientPreface.size()) {
172
0
      return;
173
0
    }
174
34.2k
    if (std::string_view(client_bytes_).substr(0, kClientPreface.size()) == kClientPreface) {
175
34.2k
      client_bytes_.erase(0, kClientPreface.size());
176
34.2k
    }
177
34.2k
    expect_client_preface_ = false;
178
34.2k
  }
179
180
212k
  std::size_t consumed = 0;
181
597k
  while (client_bytes_.size() - consumed >= kFrameHeaderSize) {
182
426k
    const auto* header = reinterpret_cast<const unsigned char*>(client_bytes_.data() + consumed);
183
426k
    const std::size_t payload_size = (static_cast<std::size_t>(header[0]) << 16U) |
184
426k
                                     (static_cast<std::size_t>(header[1]) << 8U) | static_cast<std::size_t>(header[2]);
185
426k
    if (payload_size > scenario_limits::kMaxHttp2ObservedBytes ||
186
426k
        client_bytes_.size() - consumed < kFrameHeaderSize + payload_size) {
187
40.7k
      break;
188
40.7k
    }
189
385k
    const std::uint8_t type = header[3];
190
385k
    const std::uint8_t flags = header[4];
191
385k
    const std::uint32_t stream_id = ReadUint32(client_bytes_.data() + consumed + 5) & 0x7fffffffU;
192
385k
    RecordClientFrame(type, flags, stream_id);
193
385k
    consumed += kFrameHeaderSize + payload_size;
194
385k
  }
195
212k
  client_bytes_.erase(0, consumed);
196
212k
}
197
198
385k
void H2PlanDriver::RecordClientFrame(std::uint8_t type, std::uint8_t flags, std::uint32_t stream_id) {
199
385k
  if (type == kSettingsFrame && stream_id == 0 && (flags & kFlagAck) == 0) {
200
34.2k
    ++observed_client_settings_;
201
34.2k
    return;
202
34.2k
  }
203
351k
  if (stream_id == 0) {
204
70.8k
    return;
205
70.8k
  }
206
207
280k
  auto& stream = client_streams_[stream_id];
208
280k
  if (type == kHeadersFrame) {
209
71.8k
    ++stream.headers;
210
71.8k
    if ((stream_id & 1U) != 0 &&
211
71.8k
        std::find(request_streams_.begin(), request_streams_.end(), stream_id) == request_streams_.end()) {
212
71.8k
      request_streams_.push_back(stream_id);
213
71.8k
    }
214
208k
  } else if (type == kDataFrame) {
215
45.3k
    ++stream.data;
216
45.3k
  }
217
280k
  if ((type == kHeadersFrame || type == kDataFrame) && (flags & kFlagEndStream) != 0) {
218
69.7k
    ++stream.end_stream;
219
69.7k
  }
220
280k
}
221
222
std::uint32_t H2PlanDriver::ResolveStream(const curl::fuzzer::proto::Http2StreamRef& stream,
223
72.4k
                                          bool allow_connection) const {
224
72.4k
  using StreamRef = curl::fuzzer::proto::Http2StreamRef;
225
72.4k
  switch (stream.target_case()) {
226
1.58k
    case StreamRef::kConnection:
227
1.58k
      return allow_connection ? 0U : 1U;
228
2.12k
    case StreamRef::kRequestIndex: {
229
2.12k
      const std::size_t index = static_cast<std::size_t>(stream.request_index());
230
2.12k
      if (index < request_streams_.size()) {
231
1.49k
        return request_streams_[index];
232
1.49k
      }
233
626
      return 1U + 2U * static_cast<std::uint32_t>(index % 0x3fffffffU);
234
2.12k
    }
235
2.59k
    case StreamRef::kPushIndex: {
236
2.59k
      const std::size_t index = static_cast<std::size_t>(stream.push_index());
237
2.59k
      if (index < pushed_streams_.size()) {
238
1.29k
        return pushed_streams_[index];
239
1.29k
      }
240
1.30k
      return 2U + 2U * static_cast<std::uint32_t>(index % 0x3fffffffU);
241
2.59k
    }
242
3.00k
    case StreamRef::kExplicitId:
243
3.00k
      return stream.explicit_id() & 0x7fffffffU;
244
526
    case StreamRef::kLatestRequest:
245
63.1k
    case StreamRef::TARGET_NOT_SET:
246
63.1k
    default:
247
63.1k
      return request_streams_.empty() ? 1U : request_streams_.back();
248
72.4k
  }
249
72.4k
}
250
251
9.63k
std::uint32_t H2PlanDriver::ResolvePromisedStream(const curl::fuzzer::proto::Http2StreamRef& stream) {
252
9.63k
  std::uint32_t stream_id = 0;
253
9.63k
  if (stream.target_case() == curl::fuzzer::proto::Http2StreamRef::TARGET_NOT_SET ||
254
1.86k
      stream.target_case() == curl::fuzzer::proto::Http2StreamRef::kLatestRequest ||
255
8.10k
      stream.target_case() == curl::fuzzer::proto::Http2StreamRef::kConnection) {
256
8.10k
    stream_id = 2U + 2U * static_cast<std::uint32_t>(pushed_streams_.size());
257
8.10k
  } else {
258
1.53k
    stream_id = ResolveStream(stream, false);
259
1.53k
  }
260
9.63k
  if (std::find(pushed_streams_.begin(), pushed_streams_.end(), stream_id) == pushed_streams_.end()) {
261
9.51k
    pushed_streams_.push_back(stream_id);
262
9.51k
  }
263
9.63k
  return stream_id;
264
9.63k
}
265
266
5.39k
bool H2PlanDriver::WaitSatisfied(const curl::fuzzer::proto::Http2Wait& wait) const {
267
5.39k
  const std::size_t required = std::max<std::size_t>(1, std::min<std::size_t>(wait.count(), 16));
268
5.39k
  if (wait.event() == curl::fuzzer::proto::HTTP2_CLIENT_EVENT_SETTINGS) {
269
1.98k
    return observed_client_settings_ >= required;
270
1.98k
  }
271
3.41k
  const std::uint32_t stream_id = ResolveStream(wait.stream(), false);
272
3.41k
  const auto found = client_streams_.find(stream_id);
273
3.41k
  if (found == client_streams_.end()) {
274
1.50k
    return false;
275
1.50k
  }
276
1.90k
  switch (wait.event()) {
277
943
    case curl::fuzzer::proto::HTTP2_CLIENT_EVENT_HEADERS:
278
943
      return found->second.headers >= required;
279
411
    case curl::fuzzer::proto::HTTP2_CLIENT_EVENT_DATA:
280
411
      return found->second.data >= required;
281
554
    case curl::fuzzer::proto::HTTP2_CLIENT_EVENT_END_STREAM:
282
554
      return found->second.end_stream >= required;
283
0
    case curl::fuzzer::proto::HTTP2_CLIENT_EVENT_SETTINGS:
284
0
    default:
285
0
      return false;
286
1.90k
  }
287
1.90k
}
288
289
56.0k
bool H2PlanDriver::EncodeHeaders(const std::vector<std::pair<std::string, std::string>>& headers, std::string* output) {
290
56.0k
  if (deflater_ == nullptr || output == nullptr) {
291
0
    return false;
292
0
  }
293
56.0k
  std::vector<nghttp2_nv> name_values;
294
56.0k
  name_values.reserve(headers.size());
295
146k
  for (const auto& header : headers) {
296
146k
    nghttp2_nv nv{};
297
146k
    nv.name = reinterpret_cast<std::uint8_t*>(const_cast<char*>(header.first.data()));
298
146k
    nv.value = reinterpret_cast<std::uint8_t*>(const_cast<char*>(header.second.data()));
299
146k
    nv.namelen = header.first.size();
300
146k
    nv.valuelen = header.second.size();
301
146k
    nv.flags = NGHTTP2_NV_FLAG_NONE;
302
146k
    name_values.push_back(nv);
303
146k
  }
304
56.0k
  const std::size_t bound = nghttp2_hd_deflate_bound(deflater_, name_values.data(), name_values.size());
305
56.0k
  if (bound > scenario_limits::kMaxHttp2HeaderBytes * 2U) {
306
0
    return false;
307
0
  }
308
56.0k
  output->resize(bound);
309
56.0k
  const nghttp2_ssize encoded = nghttp2_hd_deflate_hd2(deflater_, reinterpret_cast<std::uint8_t*>(output->data()),
310
56.0k
                                                       output->size(), name_values.data(), name_values.size());
311
56.0k
  if (encoded < 0) {
312
0
    output->clear();
313
0
    return false;
314
0
  }
315
56.0k
  output->resize(static_cast<std::size_t>(encoded));
316
56.0k
  return true;
317
56.0k
}
318
319
107k
bool H2PlanDriver::SerializeAction(const curl::fuzzer::proto::Http2Action& action, std::string* output) {
320
107k
  using Action = curl::fuzzer::proto::Http2Action;
321
107k
  output->clear();
322
107k
  switch (action.action_case()) {
323
26.3k
    case Action::kSettings: {
324
26.3k
      std::string payload;
325
26.3k
      const auto& settings = action.settings();
326
26.3k
      if (!settings.ack()) {
327
26.0k
        const std::size_t count = std::min<std::size_t>(scenario_limits::kMaxHttp2Settings,
328
26.0k
                                                        static_cast<std::size_t>(settings.entries_size()));
329
26.0k
        payload.reserve(count * 6U);
330
39.3k
        for (std::size_t index = 0; index < count; ++index) {
331
13.2k
          const auto& entry = settings.entries(static_cast<int>(index));
332
13.2k
          const std::uint16_t identifier = static_cast<std::uint16_t>(entry.identifier());
333
13.2k
          payload.push_back(static_cast<char>((identifier >> 8U) & 0xffU));
334
13.2k
          payload.push_back(static_cast<char>(identifier & 0xffU));
335
13.2k
          AppendUint32(&payload, entry.value());
336
13.2k
        }
337
26.0k
      }
338
26.3k
      AppendFrame(output, payload.size(), kSettingsFrame, settings.ack() ? kFlagAck : 0, 0, payload);
339
26.3k
      return true;
340
0
    }
341
46.4k
    case Action::kHeaders: {
342
46.4k
      const auto& headers = action.headers();
343
46.4k
      std::vector<std::pair<std::string, std::string>> fields;
344
46.4k
      const std::size_t count =
345
46.4k
          std::min<std::size_t>(scenario_limits::kMaxHttp2Headers, static_cast<std::size_t>(headers.fields_size()));
346
46.4k
      fields.reserve(count + 1U);
347
46.4k
      if (!headers.trailers()) {
348
45.7k
        fields.emplace_back(":status", std::to_string(CanonicalStatus(headers.status_code())));
349
45.7k
      }
350
46.4k
      std::size_t bytes = 0;
351
99.1k
      for (std::size_t index = 0; index < count && bytes < scenario_limits::kMaxHttp2HeaderBytes; ++index) {
352
52.7k
        const auto& field = headers.fields(static_cast<int>(index));
353
52.7k
        std::string name = CanonicalHeaderName(field.name());
354
52.7k
        std::string value = CanonicalHeaderValue(field.value());
355
52.7k
        const std::size_t remaining = scenario_limits::kMaxHttp2HeaderBytes - bytes;
356
52.7k
        if (name.size() + value.size() > remaining) {
357
0
          value.resize(remaining > name.size() ? remaining - name.size() : 0);
358
0
          name.resize(std::min(name.size(), remaining));
359
0
        }
360
52.7k
        bytes += name.size() + value.size();
361
52.7k
        fields.emplace_back(std::move(name), std::move(value));
362
52.7k
      }
363
46.4k
      std::string block;
364
46.4k
      if (!EncodeHeaders(fields, &block)) {
365
0
        return false;
366
0
      }
367
46.4k
      const std::uint32_t stream_id = ResolveStream(headers.stream(), false);
368
46.4k
      std::size_t offset = 0;
369
46.4k
      do {
370
46.4k
        const std::size_t chunk_size = std::min(kMaxFramePayload, block.size() - offset);
371
46.4k
        const bool last = offset + chunk_size == block.size();
372
46.4k
        std::uint8_t flags = last ? kFlagEndHeaders : 0;
373
46.4k
        if (offset == 0 && headers.end_stream()) {
374
38.0k
          flags |= kFlagEndStream;
375
38.0k
        }
376
46.4k
        AppendFrame(output, chunk_size, offset == 0 ? kHeadersFrame : kContinuationFrame, flags, stream_id,
377
46.4k
                    std::string_view(block).substr(offset, chunk_size));
378
46.4k
        offset += chunk_size;
379
46.4k
      } while (offset < block.size());
380
46.4k
      return true;
381
46.4k
    }
382
1.95k
    case Action::kData: {
383
1.95k
      const auto& data = action.data();
384
1.95k
      const std::string payload = BoundedString(data.data(), scenario_limits::kMaxHttp2DataBytes);
385
1.95k
      const std::uint32_t stream_id = ResolveStream(data.stream(), false);
386
1.95k
      std::size_t offset = 0;
387
1.96k
      do {
388
1.96k
        const std::size_t chunk_size = std::min(kMaxFramePayload, payload.size() - offset);
389
1.96k
        const bool last = offset + chunk_size == payload.size();
390
1.96k
        AppendFrame(output, chunk_size, kDataFrame, last && data.end_stream() ? kFlagEndStream : 0, stream_id,
391
1.96k
                    std::string_view(payload).substr(offset, chunk_size));
392
1.96k
        offset += chunk_size;
393
1.96k
      } while (offset < payload.size());
394
1.95k
      return true;
395
46.4k
    }
396
2.96k
    case Action::kWindowUpdate: {
397
2.96k
      const auto& update = action.window_update();
398
2.96k
      std::string payload;
399
2.96k
      AppendUint32(&payload, std::max(1U, update.increment() & 0x7fffffffU));
400
2.96k
      AppendFrame(output, payload.size(), kWindowUpdateFrame, 0, ResolveStream(update.stream(), true), payload);
401
2.96k
      return true;
402
46.4k
    }
403
3.15k
    case Action::kRstStream: {
404
3.15k
      const auto& reset = action.rst_stream();
405
3.15k
      std::string payload;
406
3.15k
      AppendUint32(&payload, reset.error_code());
407
3.15k
      AppendFrame(output, payload.size(), kRstStreamFrame, 0, ResolveStream(reset.stream(), false), payload);
408
3.15k
      return true;
409
46.4k
    }
410
5.64k
    case Action::kPing: {
411
5.64k
      std::array<char, 8> opaque{};
412
5.64k
      const std::string& source = action.ping().opaque_data();
413
5.64k
      std::copy_n(source.begin(), std::min(source.size(), opaque.size()), opaque.begin());
414
5.64k
      AppendFrame(output, opaque.size(), kPingFrame, action.ping().ack() ? kFlagAck : 0, 0,
415
5.64k
                  std::string_view(opaque.data(), opaque.size()));
416
5.64k
      return true;
417
46.4k
    }
418
3.32k
    case Action::kGoaway: {
419
3.32k
      const auto& goaway = action.goaway();
420
3.32k
      std::string payload;
421
3.32k
      AppendUint32(&payload, ResolveStream(goaway.last_stream(), true));
422
3.32k
      AppendUint32(&payload, goaway.error_code());
423
3.32k
      payload.append(BoundedString(goaway.debug_data(), scenario_limits::kMaxHttp2RawFrameBytes));
424
3.32k
      AppendFrame(output, payload.size(), kGoawayFrame, 0, 0, payload);
425
3.32k
      return true;
426
46.4k
    }
427
9.63k
    case Action::kPushPromise: {
428
9.63k
      const auto& promise = action.push_promise();
429
9.63k
      std::vector<std::pair<std::string, std::string>> fields = {
430
9.63k
          {":method", "GET"}, {":scheme", scheme_}, {":authority", authority_}, {":path", "/push"}};
431
9.63k
      const std::size_t count = std::min<std::size_t>(scenario_limits::kMaxHttp2Headers,
432
9.63k
                                                      static_cast<std::size_t>(promise.request_headers_size()));
433
19.2k
      for (std::size_t index = 0; index < count; ++index) {
434
9.60k
        const auto& field = promise.request_headers(static_cast<int>(index));
435
9.60k
        fields.emplace_back(CanonicalHeaderName(field.name()), CanonicalHeaderValue(field.value()));
436
9.60k
      }
437
9.63k
      std::string block;
438
9.63k
      if (!EncodeHeaders(fields, &block)) {
439
0
        return false;
440
0
      }
441
9.63k
      const std::uint32_t parent = ResolveStream(promise.parent_stream(), false);
442
9.63k
      const std::uint32_t promised = ResolvePromisedStream(promise.promised_stream());
443
9.63k
      std::string first_payload;
444
9.63k
      AppendUint32(&first_payload, promised);
445
9.63k
      const std::size_t first_size = std::min(kMaxFramePayload - 4U, block.size());
446
9.63k
      first_payload.append(block.data(), first_size);
447
9.63k
      AppendFrame(output, first_payload.size(), kPushPromiseFrame, first_size == block.size() ? kFlagEndHeaders : 0,
448
9.63k
                  parent, first_payload);
449
9.63k
      std::size_t offset = first_size;
450
9.64k
      while (offset < block.size()) {
451
10
        const std::size_t chunk_size = std::min(kMaxFramePayload, block.size() - offset);
452
10
        offset += chunk_size;
453
10
        AppendFrame(output, chunk_size, kContinuationFrame, offset == block.size() ? kFlagEndHeaders : 0, parent,
454
10
                    std::string_view(block).substr(offset - chunk_size, chunk_size));
455
10
      }
456
9.63k
      return true;
457
9.63k
    }
458
5.92k
    case Action::kWireFrame: {
459
5.92k
      const auto& frame = action.wire_frame();
460
5.92k
      const std::string payload = BoundedString(frame.payload(), scenario_limits::kMaxHttp2RawFrameBytes);
461
5.92k
      const std::uint32_t declared = frame.use_declared_length() ? frame.declared_length() : payload.size();
462
5.92k
      AppendFrame(output, declared, static_cast<std::uint8_t>(frame.type()), static_cast<std::uint8_t>(frame.flags()),
463
5.92k
                  frame.stream_id(), payload);
464
5.92k
      return true;
465
9.63k
    }
466
1.94k
    case Action::kRawBytes:
467
1.94k
      *output = BoundedString(action.raw_bytes(), scenario_limits::kMaxHttp2RawFrameBytes);
468
1.94k
      return true;
469
0
    case Action::kWait:
470
0
    case Action::kYieldTurns:
471
0
    case Action::ACTION_NOT_SET:
472
0
    default:
473
0
      return false;
474
107k
  }
475
107k
}
476
477
/// Advance one scripted boundary. `output` receives zero or more complete
478
/// HTTP/2 frames. A true result also represents a consumed wait or yield;
479
/// false means the next barrier is not ready or the plan is complete.
480
/// @param output Destination for the serialized frame bytes.
481
/// @return true when a frame, wait, or yield boundary was consumed.
482
365k
bool H2PlanDriver::NextChunk(std::string* output) {
483
365k
  if (output == nullptr || plan_ == nullptr) {
484
0
    return false;
485
0
  }
486
365k
  output->clear();
487
365k
  if (!initial_settings_sent_) {
488
19.6k
    curl::fuzzer::proto::Http2Action initial;
489
19.6k
    *initial.mutable_settings() = plan_->initial_settings();
490
19.6k
    initial.mutable_settings()->set_ack(false);
491
19.6k
    (void)SerializeAction(initial, output);
492
19.6k
    initial_settings_sent_ = true;
493
19.6k
    return true;
494
19.6k
  }
495
346k
  if (!client_settings_ack_sent_) {
496
19.6k
    if (observed_client_settings_ == 0) {
497
0
      return false;
498
0
    }
499
19.6k
    AppendFrame(output, 0, kSettingsFrame, kFlagAck, 0, {});
500
19.6k
    client_settings_ack_sent_ = true;
501
19.6k
    return true;
502
19.6k
  }
503
504
326k
  const std::size_t action_count =
505
326k
      std::min<std::size_t>(scenario_limits::kMaxHttp2Actions, static_cast<std::size_t>(plan_->actions_size()));
506
326k
  if (next_action_ >= action_count) {
507
101k
    return false;
508
101k
  }
509
224k
  const auto& action = plan_->actions(static_cast<int>(next_action_));
510
224k
  if (action.action_case() == curl::fuzzer::proto::Http2Action::kWait) {
511
5.39k
    if (!WaitSatisfied(action.wait())) {
512
3.65k
      return false;
513
3.65k
    }
514
1.74k
    ++next_action_;
515
1.74k
    return true;
516
5.39k
  }
517
219k
  if (action.action_case() == curl::fuzzer::proto::Http2Action::kYieldTurns) {
518
131k
    if (remaining_yields_ == 0) {
519
10.7k
      remaining_yields_ = std::min<std::size_t>(scenario_limits::kMaxHttp2YieldTurns, action.yield_turns());
520
10.7k
      if (remaining_yields_ == 0) {
521
1.52k
        ++next_action_;
522
1.52k
        return true;
523
1.52k
      }
524
10.7k
    }
525
130k
    --remaining_yields_;
526
130k
    if (remaining_yields_ == 0) {
527
8.94k
      ++next_action_;
528
8.94k
    }
529
130k
    return true;
530
131k
  }
531
532
87.6k
  (void)SerializeAction(action, output);
533
87.6k
  ++next_action_;
534
87.6k
  return true;
535
219k
}
536
537
/// @return true while startup or a bounded action remains to be processed.
538
36.0k
bool H2PlanDriver::has_pending_work() const {
539
36.0k
  if (plan_ == nullptr || !initial_settings_sent_ || !client_settings_ack_sent_) {
540
16.4k
    return plan_ != nullptr;
541
16.4k
  }
542
19.6k
  return next_action_ <
543
19.6k
         std::min<std::size_t>(scenario_limits::kMaxHttp2Actions, static_cast<std::size_t>(plan_->actions_size()));
544
36.0k
}
545
546
/// @return number of distinct client request streams observed.
547
36.0k
std::size_t H2PlanDriver::observed_request_count() const { return request_streams_.size(); }
548
549
/// @return number of non-ACK client SETTINGS frames observed.
550
36.0k
std::size_t H2PlanDriver::observed_client_settings_count() const { return observed_client_settings_; }
551
552
}  // namespace proto_fuzzer