Coverage Report

Created: 2026-10-28 07:17

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/curl_fuzzer/proto_fuzzer/rtsp_mock_server.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 Implementation of the request-aligned in-process RTSP peer.
9
10
#include "proto_fuzzer/rtsp_mock_server.h"
11
12
#include <algorithm>
13
#include <cstddef>
14
#include <cstdint>
15
#include <limits>
16
#include <string>
17
#include <string_view>
18
19
namespace proto_fuzzer {
20
21
namespace {
22
23
/// Remove a command line's trailing CR/LF so header parsing does not treat the
24
/// terminator as part of a value.
25
/// @param line Complete newline-terminated line.
26
/// @return The line without its line ending.
27
136k
std::string_view TrimLineEnding(std::string_view line) {
28
365k
  while (!line.empty() && (line.back() == '\n' || line.back() == '\r')) {
29
228k
    line.remove_suffix(1);
30
228k
  }
31
136k
  return line;
32
136k
}
33
34
/// Compare two ASCII tokens ignoring case. Header names are case-insensitive,
35
/// and protocol bytes outside ASCII have no valid interpretation here.
36
/// @param actual Observed token.
37
/// @param expected Literal comparison token.
38
/// @return true when both tokens contain the same case-folded ASCII bytes.
39
15.8k
bool EqualsIgnoreCase(std::string_view actual, std::string_view expected) {
40
15.8k
  if (actual.size() != expected.size()) {
41
0
    return false;
42
0
  }
43
101k
  for (std::size_t index = 0; index < actual.size(); ++index) {
44
88.0k
    unsigned char left = static_cast<unsigned char>(actual[index]);
45
88.0k
    unsigned char right = static_cast<unsigned char>(expected[index]);
46
88.0k
    if (left >= 'a' && left <= 'z') {
47
55.3k
      left = static_cast<unsigned char>(left - 'a' + 'A');
48
55.3k
    }
49
88.0k
    if (right >= 'a' && right <= 'z') {
50
55.1k
      right = static_cast<unsigned char>(right - 'a' + 'A');
51
55.1k
    }
52
88.0k
    if (left != right) {
53
2.58k
      return false;
54
2.58k
    }
55
88.0k
  }
56
13.3k
  return true;
57
15.8k
}
58
59
/// Extract a header value from one complete header line. Only an exact
60
/// case-insensitive field name followed by a colon matches, so a value that
61
/// happens to contain the same text cannot be mistaken for the header.
62
/// @param line Complete header line, including its line ending.
63
/// @param name Case-insensitive field name without the colon.
64
/// @param value Receives the trimmed value when the header matches.
65
/// @return true when the field name matched.
66
65.7k
bool HeaderValue(std::string_view line, std::string_view name, std::string_view* value) {
67
65.7k
  const std::string_view trimmed = TrimLineEnding(line);
68
65.7k
  if (trimmed.size() <= name.size() || trimmed[name.size()] != ':' ||
69
52.4k
      !EqualsIgnoreCase(trimmed.substr(0, name.size()), name)) {
70
52.4k
    return false;
71
52.4k
  }
72
73
13.3k
  std::size_t begin = name.size() + 1;
74
49.3k
  while (begin < trimmed.size() && (trimmed[begin] == ' ' || trimmed[begin] == '\t')) {
75
36.0k
    ++begin;
76
36.0k
  }
77
13.3k
  std::string_view parsed = trimmed.substr(begin);
78
19.4k
  while (!parsed.empty() && (parsed.back() == ' ' || parsed.back() == '\t')) {
79
6.10k
    parsed.remove_suffix(1);
80
6.10k
  }
81
13.3k
  *value = parsed;
82
13.3k
  return true;
83
65.7k
}
84
85
/// Parse a non-negative decimal field without relying on locale-sensitive or
86
/// throwing library conversions. Oversized values saturate, because the
87
/// caller only needs a bounded amount of subsequent work.
88
/// @param text Decimal digits with no sign or other characters.
89
/// @param result Receives the parsed or saturated value.
90
/// @return true when every byte was a decimal digit and the text was nonempty.
91
13.3k
bool ParseDecimal(std::string_view text, std::uint64_t* result) {
92
13.3k
  if (text.empty()) {
93
847
    return false;
94
847
  }
95
12.4k
  std::uint64_t value = 0;
96
26.8k
  for (const char byte : text) {
97
26.8k
    if (byte < '0' || byte > '9') {
98
1.80k
      return false;
99
1.80k
    }
100
25.0k
    const std::uint64_t digit = static_cast<std::uint64_t>(byte - '0');
101
25.0k
    if (value > (std::numeric_limits<std::uint64_t>::max() - digit) / 10U) {
102
1.22k
      value = std::numeric_limits<std::uint64_t>::max();
103
23.7k
    } else {
104
23.7k
      value = value * 10U + digit;
105
23.7k
    }
106
25.0k
  }
107
10.6k
  *result = value;
108
10.6k
  return true;
109
12.4k
}
110
111
/// Recognize the empty line that terminates an RTSP request header block.
112
/// @param line Complete line, including its line ending.
113
/// @return true when the line contains no bytes before its line ending.
114
54.4k
bool IsBlankLine(std::string_view line) { return TrimLineEnding(line).empty(); }
115
116
}  // namespace
117
118
/// Begin without a borrowed protobuf or open socketpair. RunLoop borrows a
119
/// Scenario only for the synchronous drive in which socket callbacks can run.
120
RtspMockServer::RtspMockServer()
121
6.30k
    : in_request_(false),
122
6.30k
      in_headers_(false),
123
6.30k
      have_client_cseq_(false),
124
6.30k
      have_content_length_(false),
125
6.30k
      client_cseq_(0),
126
6.30k
      last_client_cseq_(0),
127
6.30k
      content_length_(0),
128
6.30k
      request_count_(0),
129
6.30k
      response_count_(0) {}
130
131
/// Release the server-owned socketpair half after curl has returned its client
132
/// descriptor. The out-of-line destructor keeps inherited transport ownership
133
/// local to this translation unit.
134
6.30k
RtspMockServer::~RtspMockServer() = default;
135
136
/// Expose how far the bounded request stream progressed. The parser counts a
137
/// request only after its header block and any announced body were consumed.
138
/// @return Complete requests observed in the most recent drive.
139
6.27k
std::size_t RtspMockServer::request_count() const { return request_count_; }
140
141
/// Expose how many scripted replies could be released before the bounded
142
/// script ran out.
143
/// @return Scripted replies written in the most recent drive.
144
6.27k
std::size_t RtspMockServer::response_count() const { return response_count_; }
145
146
/// Expose the last parsed client sequence number so tests can verify that one
147
/// request advanced the exchange exactly once.
148
/// @return Last request CSeq, or zero when no parseable CSeq was present.
149
6.27k
std::uint32_t RtspMockServer::last_client_cseq() const { return last_client_cseq_; }
150
151
/// @return Start line of the most recently completed request.
152
6.27k
const std::string& RtspMockServer::last_request_line() const { return last_request_line_; }
153
154
/// @return Method token of the most recently completed request.
155
0
const std::string& RtspMockServer::last_method() const { return last_method_; }
156
157
/// @return Stream URI token of the most recently completed request.
158
6.27k
const std::string& RtspMockServer::last_stream_uri() const { return last_stream_uri_; }
159
160
/// Clear RTSP's per-request parse state before the next drive. The shared base
161
/// has already reset the transport, reply cursor, transcript, upload capture,
162
/// and body mode.
163
6.17k
void RtspMockServer::ResetProtocolState() {
164
6.17k
  in_request_ = false;
165
6.17k
  in_headers_ = false;
166
6.17k
  have_client_cseq_ = false;
167
6.17k
  have_content_length_ = false;
168
6.17k
  client_cseq_ = 0;
169
6.17k
  last_client_cseq_ = 0;
170
6.17k
  content_length_ = 0;
171
6.17k
  request_count_ = 0;
172
6.17k
  response_count_ = 0;
173
6.17k
  last_request_line_.clear();
174
6.17k
  last_method_.clear();
175
6.17k
  last_stream_uri_.clear();
176
6.17k
}
177
178
/// Start a new request after the previous one completed. The request line is
179
/// parsed for observability, while the reply itself stays scripted.
180
/// @param line Complete request start line, including its line ending.
181
16.7k
void RtspMockServer::BeginRequest(std::string_view line) {
182
16.7k
  const std::string_view trimmed = TrimLineEnding(line);
183
16.7k
  last_request_line_.assign(trimmed.data(), trimmed.size());
184
16.7k
  const std::string_view method = FirstToken(trimmed);
185
16.7k
  const std::string_view stream_uri = SecondToken(trimmed);
186
16.7k
  last_method_.assign(method.data(), method.size());
187
16.7k
  last_stream_uri_.assign(stream_uri.data(), stream_uri.size());
188
189
16.7k
  in_request_ = true;
190
16.7k
  in_headers_ = true;
191
16.7k
  have_client_cseq_ = false;
192
16.7k
  have_content_length_ = false;
193
16.7k
  client_cseq_ = 0;
194
16.7k
  content_length_ = 0;
195
16.7k
}
196
197
/// Parse the two headers that change how the peer stays synchronized. Other
198
/// headers remain part of the transcript and are otherwise opaque to the peer.
199
/// @param line Complete header line, including its line ending.
200
38.0k
void RtspMockServer::ParseHeader(std::string_view line) {
201
38.0k
  std::string_view value;
202
38.0k
  if (HeaderValue(line, "CSeq", &value)) {
203
10.3k
    std::uint64_t parsed = 0;
204
10.3k
    if (ParseDecimal(value, &parsed)) {
205
8.37k
      have_client_cseq_ = true;
206
8.37k
      client_cseq_ =
207
8.37k
          static_cast<std::uint32_t>(std::min<std::uint64_t>(parsed, std::numeric_limits<std::uint32_t>::max()));
208
8.37k
    }
209
10.3k
    return;
210
10.3k
  }
211
212
27.7k
  if (HeaderValue(line, "Content-Length", &value)) {
213
3.00k
    std::uint64_t parsed = 0;
214
3.00k
    if (ParseDecimal(value, &parsed)) {
215
2.27k
      have_content_length_ = true;
216
2.27k
      content_length_ =
217
2.27k
          static_cast<std::size_t>(std::min<std::uint64_t>(parsed, std::numeric_limits<std::size_t>::max()));
218
2.27k
    }
219
3.00k
  }
220
27.7k
}
221
222
/// Complete one request: advance the scripted reply cursor, record the parsed
223
/// exchange, and return to start-line mode. An empty scripted reply still
224
/// advances the cursor because it is a meaningful truncation mutation.
225
16.2k
void RtspMockServer::FinishRequest() {
226
16.2k
  ++request_count_;
227
16.2k
  last_client_cseq_ = have_client_cseq_ ? client_cseq_ : 0;
228
16.2k
  if (WriteNextReply()) {
229
3.78k
    ++response_count_;
230
3.78k
  }
231
232
16.2k
  in_request_ = false;
233
16.2k
  in_headers_ = false;
234
16.2k
  have_client_cseq_ = false;
235
16.2k
  have_content_length_ = false;
236
16.2k
  client_cseq_ = 0;
237
16.2k
  content_length_ = 0;
238
16.2k
}
239
240
/// Consume one complete request line or header line. The blank line that ends
241
/// the header block is the reply trigger: an announced body first switches the
242
/// shared parser into exact-length mode, whose completion calls
243
/// HandleBodyComplete().
244
/// @param line Complete line sent by curl.
245
71.2k
void RtspMockServer::HandleLine(std::string_view line) {
246
71.2k
  if (!in_request_) {
247
16.7k
    BeginRequest(line);
248
16.7k
    return;
249
16.7k
  }
250
251
54.4k
  if (IsBlankLine(line)) {
252
16.4k
    in_headers_ = false;
253
16.4k
    if (have_content_length_ && content_length_ != 0) {
254
1.53k
      BeginExactBody(content_length_);
255
1.53k
      return;
256
1.53k
    }
257
14.9k
    FinishRequest();
258
14.9k
    return;
259
16.4k
  }
260
261
38.0k
  if (in_headers_) {
262
38.0k
    ParseHeader(line);
263
38.0k
  }
264
38.0k
}
265
266
/// Release the reply after an announced request body was drained exactly.
267
1.27k
void RtspMockServer::HandleBodyComplete() { FinishRequest(); }
268
269
}  // namespace proto_fuzzer