19namespace proto_fuzzer {
26bool AsciiEqualsIgnoreCase(std::string_view value, std::string_view expected_lowercase) {
27 if (value.size() != expected_lowercase.size()) {
30 for (std::size_t index = 0; index < value.size(); ++index) {
31 unsigned char actual =
static_cast<unsigned char>(value[index]);
32 if (actual >=
'A' && actual <=
'Z') {
33 actual =
static_cast<unsigned char>(actual -
'A' +
'a');
35 if (actual !=
static_cast<unsigned char>(expected_lowercase[index])) {
59 authentication_continuation_active_ =
false;
67bool ImapMockServer::StatusLineNeedsTag(std::string_view line) {
68 const std::size_t space = line.find_first_of(
" \r\n");
69 const std::string_view keyword = line.substr(0, space);
70 if (keyword.empty()) {
73 if (!AsciiEqualsIgnoreCase(keyword,
"ok") && !AsciiEqualsIgnoreCase(keyword,
"no") &&
74 !AsciiEqualsIgnoreCase(keyword,
"bad") && !AsciiEqualsIgnoreCase(keyword,
"preauth")) {
77 return space != std::string_view::npos;
85bool ImapMockServer::ResponseHasContinuation(std::string_view response) {
86 std::size_t position = 0;
87 while (position < response.size()) {
88 const std::size_t newline = response.find(
'\n', position);
89 const std::size_t line_end = newline == std::string_view::npos ? response.size() : newline + 1;
90 const std::string_view line = response.substr(position, line_end - position);
91 if (!line.empty() && line.front() ==
'+') {
95 const std::size_t literal = LiteralSize(line);
96 const std::size_t available = response.size() - line_end;
97 position = line_end + std::min(literal, available);
109bool ImapMockServer::ResponseHasTaggedCompletion(std::string_view response)
const {
110 std::size_t position = 0;
111 while (position < response.size()) {
112 const std::size_t newline = response.find(
'\n', position);
113 const std::size_t line_end = newline == std::string_view::npos ? response.size() : newline + 1;
114 const std::string_view line = response.substr(position, line_end - position);
115 const bool tag_relative = StatusLineNeedsTag(line);
116 const bool has_current_tag = !current_tag_.empty() && line.size() > current_tag_.size() &&
117 line.compare(0, current_tag_.size(), current_tag_) == 0 &&
118 line[current_tag_.size()] ==
' ' &&
119 StatusLineNeedsTag(line.substr(current_tag_.size() + 1));
120 if (tag_relative || has_current_tag) {
124 const std::size_t literal = LiteralSize(line);
125 const std::size_t available = response.size() - line_end;
126 position = line_end + std::min(literal, available);
134std::size_t ImapMockServer::LiteralSize(std::string_view line) {
135 while (!line.empty() && (line.back() ==
'\r' || line.back() ==
'\n')) {
136 line.remove_suffix(1);
138 if (line.empty() || line.back() !=
'}') {
141 const std::size_t open = line.rfind(
'{');
142 if (open == std::string_view::npos || open + 2 > line.size()) {
146 std::size_t position = open + 1;
147 std::size_t value = 0;
149 while (position < line.size() && line[position] >=
'0' && line[position] <=
'9') {
150 const std::size_t digit =
static_cast<std::size_t
>(line[position] -
'0');
151 if (value > (std::numeric_limits<std::size_t>::max() - digit) / 10) {
154 value = value * 10 + digit;
161 if (position < line.size() && line[position] ==
'+') {
164 return position + 1 == line.size() ? value : 0;
172std::size_t ImapMockServer::AppendLiteralSize(std::string_view command) {
173 while (!command.empty() && (command.back() ==
'\r' || command.back() ==
'\n')) {
174 command.remove_suffix(1);
176 if (command.empty() || command.back() !=
'}') {
177 return std::numeric_limits<std::size_t>::max();
179 const std::size_t open = command.rfind(
'{');
180 if (open == std::string_view::npos) {
181 return std::numeric_limits<std::size_t>::max();
184 std::size_t position = open + 1;
185 std::size_t value = 0;
187 while (position < command.size() && command[position] >=
'0' && command[position] <=
'9') {
188 const std::size_t digit =
static_cast<std::size_t
>(command[position] -
'0');
189 if (value > (std::numeric_limits<std::size_t>::max() - digit) / 10) {
190 return std::numeric_limits<std::size_t>::max();
192 value = value * 10 + digit;
197 return std::numeric_limits<std::size_t>::max();
199 if (position < command.size() && command[position] ==
'+') {
202 return position + 1 == command.size() ? value : std::numeric_limits<std::size_t>::max();
210std::string ImapMockServer::RewriteReply(std::string_view response)
const {
212 output.reserve(response.size() + 16);
214 std::size_t position = 0;
215 while (position < response.size()) {
216 const std::size_t newline = response.find(
'\n', position);
217 const std::size_t line_end = newline == std::string_view::npos ? response.size() : newline + 1;
218 const std::string_view line = response.substr(position, line_end - position);
220 if (!current_tag_.empty() && StatusLineNeedsTag(line)) {
221 output.append(current_tag_);
222 output.push_back(
' ');
226 const std::size_t literal = LiteralSize(line);
232 const std::size_t body_start = line_end;
233 const std::size_t available = response.size() - body_start;
234 const std::size_t body_size = std::min(literal, available);
235 output.append(response.substr(body_start, body_size));
236 position = body_start + body_size;
247std::string ImapMockServer::NextCommandTag()
const {
248 std::size_t digits_start = current_tag_.size();
249 while (digits_start > 0 && current_tag_[digits_start - 1] >=
'0' && current_tag_[digits_start - 1] <=
'9') {
252 if (digits_start == current_tag_.size() || digits_start == 0) {
253 return std::string();
256 std::uint64_t number = 0;
257 for (std::size_t index = digits_start; index < current_tag_.size(); ++index) {
258 number = number * 10 +
static_cast<std::uint64_t
>(current_tag_[index] -
'0');
259 if (number > 999999) {
260 return std::string();
265 std::string digits = std::to_string(number);
266 const std::size_t width = current_tag_.size() - digits_start;
267 if (digits.size() < width) {
268 digits.insert(0, width - digits.size(),
'0');
270 return current_tag_.substr(0, digits_start) + digits;
285 const bool authentication_payload = authentication_continuation_active_;
286 if (!authentication_payload) {
287 current_tag_ = std::string(
FirstToken(command));
291 if (response ==
nullptr) {
295 const bool continuation = ResponseHasContinuation(*response);
296 const bool tagged_completion = ResponseHasTaggedCompletion(*response);
299 if (authentication_payload) {
300 if (tagged_completion) {
301 authentication_continuation_active_ =
false;
306 const std::string_view verb =
SecondToken(command);
307 if (continuation &&
TokenEquals(verb,
"AUTHENTICATE")) {
308 authentication_continuation_active_ = !tagged_completion;
312 if (!continuation || !
TokenEquals(verb,
"APPEND")) {
316 const std::size_t literal = AppendLiteralSize(command);
317 if (literal == std::numeric_limits<std::size_t>::max()) {
327 if (final_response !=
nullptr) {
336 const std::string next_tag = NextCommandTag();
337 if (!next_tag.empty()) {
346#if defined(PROTO_FUZZER_OBSERVE_STATE)
349 (void)imap_observation;
void ObserveState() override
Retain protocol outcomes in coverage and introspector builds.
void HandleLine(std::string_view line) override
Advance one reply for every complete command or AUTHENTICATE continuation.
void QueueShutdownReply() override
Queue a tagged completion for curl's blocking LOGOUT during teardown.
~ImapMockServer() override
Release the server-owned socketpair half after curl has returned its client descriptor.
void ResetProtocolState() override
Clear the tag remembered from the previous drive.
ImapMockServer()
Begin without a borrowed protobuf or open socketpair.
virtual void ObserveState()
Default no-op for peers without protocol-specific observations.
const std::string & control_transcript() const
Expose the bounded command capture from the most recent drive so tests can verify protocol states wit...
const curl::fuzzer::proto::Scenario * scenario() const
Borrowed scenario for the synchronous drive, or nullptr outside RunLoop().
void BeginLiteralBody(std::size_t length)
Enter length-announced literal body mode.
static std::string_view SecondToken(std::string_view line)
Skip the first token and return the second whitespace-delimited token without allocating.
static std::string_view FirstToken(std::string_view line)
Remove optional leading horizontal whitespace and return the first token without allocating.
const std::string * NextControlReply()
Return the next command-aligned response within the shared response-count budget.
const std::string & uploaded_data() const
Expose a bounded prefix of bytes drained during the most recent body phase while continuing to drain ...
static bool TokenEquals(std::string_view token, std::string_view expected_uppercase)
Fold only ASCII lowercase command letters.
bool WriteControlBytes(std::string_view bytes)
Keep failed peer writes local to the mock.
Command-aware in-process peer for curl's IMAP state machine.