/* * Copyright (c) 2021, Kyle Pereira * * SPDX-License-Identifier: BSD-2-Clause */ #include #include #include namespace IMAP { ParseStatus Parser::parse(ByteBuffer&& buffer, bool expecting_tag) { auto response_or_error = try_parse(move(buffer), expecting_tag); if (response_or_error.is_error()) return { false, {} }; auto response = response_or_error.release_value(); return response; } ErrorOr Parser::try_parse(ByteBuffer&& buffer, bool expecting_tag) { dbgln_if(IMAP_PARSER_DEBUG, "Parser received {} bytes:\n\"{}\"", buffer.size(), StringView(buffer.data(), buffer.size())); if (m_incomplete) { m_buffer += buffer; m_incomplete = false; } else { m_buffer = move(buffer); m_position = 0; m_response = SolidResponse(); } if (consume_if("+"sv)) { TRY(consume(" "sv)); auto data = consume_until_end_of_line(); TRY(consume(" "sv)); return ParseStatus { true, { ContinueRequest { data } } }; } while (consume_if("*"sv)) { TRY(parse_untagged()); } if (expecting_tag) { if (at_end()) { m_incomplete = true; return ParseStatus { true, {} }; } TRY(parse_response_done()); } return ParseStatus { true, { move(m_response) } }; } bool Parser::consume_if(StringView x) { dbgln_if(IMAP_PARSER_DEBUG, "p: {}, consume({})", m_position, x); size_t i = 0; auto previous_position = m_position; while (i < x.length() && !at_end() && to_ascii_lowercase(x[i]) == to_ascii_lowercase(m_buffer[m_position])) { i++; m_position++; } if (i != x.length()) { // We didn't match the full string. m_position = previous_position; dbgln_if(IMAP_PARSER_DEBUG, "ret false"); return false; } dbgln_if(IMAP_PARSER_DEBUG, "ret true"); return true; } ErrorOr Parser::parse_response_done() { TRY(consume("A"sv)); auto tag = TRY(parse_number()); TRY(consume(" "sv)); ResponseStatus status = TRY(parse_status()); TRY(consume(" "sv)); m_response.m_tag = tag; m_response.m_status = status; StringBuilder response_data; while (!at_end() && m_buffer[m_position] != '\r') { response_data.append((char)m_buffer[m_position]); m_position += 1; } TRY(consume("\r\n"sv)); m_response.m_response_text = response_data.to_byte_string(); return {}; } ErrorOr Parser::consume(StringView x) { if (!consume_if(x)) { dbgln("\"{}\" not matched at {}, (buffer length {})", x, m_position, m_buffer.size()); return Error::from_string_literal("Token not matched"); } return {}; } Optional Parser::try_parse_number() { dbgln_if(IMAP_PARSER_DEBUG, "p: {}, try_parse_number()", m_position); auto number_matched = 0; while (!at_end() && 0 <= m_buffer[m_position] - '0' && m_buffer[m_position] - '0' <= 9) { number_matched++; m_position++; } if (number_matched == 0) { dbgln_if(IMAP_PARSER_DEBUG, "p: {}, ret empty", m_position); return {}; } auto number = StringView(m_buffer.data() + m_position - number_matched, number_matched); dbgln_if(IMAP_PARSER_DEBUG, "p: {}, ret \"{}\"", m_position, number.to_number()); return number.to_number(); } ErrorOr Parser::parse_number() { auto number = try_parse_number(); if (!number.has_value()) { dbgln("Failed to parse number at {}, (buffer length {})", m_position, m_buffer.size()); return Error::from_string_view("Failed to parse expected number"sv); } return number.value(); } ErrorOr Parser::parse_untagged() { TRY(consume(" "sv)); // Certain messages begin with a number like: // * 15 EXISTS auto number = try_parse_number(); if (number.has_value()) { TRY(consume(" "sv)); auto data_type = TRY(parse_atom()); if (data_type == "EXISTS"sv) { m_response.data().set_exists(number.value()); TRY(consume("\r\n"sv)); } else if (data_type == "RECENT"sv) { m_response.data().set_recent(number.value()); TRY(consume("\r\n"sv)); } else if (data_type == "FETCH"sv) { auto fetch_response = TRY(parse_fetch_response()); m_response.data().add_fetch_response(number.value(), move(fetch_response)); } else if (data_type == "EXPUNGE"sv) { m_response.data().add_expunged(number.value()); TRY(consume("\r\n"sv)); } return {}; } if (consume_if("CAPABILITY"sv)) { TRY(parse_capability_response()); } else if (consume_if("LIST"sv)) { auto item = TRY(parse_list_item()); m_response.data().add_list_item(move(item)); } else if (consume_if("LSUB"sv)) { auto item = TRY(parse_list_item()); m_response.data().add_lsub_item(move(item)); } else if (consume_if("FLAGS"sv)) { TRY(consume(" "sv)); auto flags = TRY(parse_list(+[](StringView x) { return ByteString(x); })); m_response.data().set_flags(move(flags)); TRY(consume("\r\n"sv)); } else if (consume_if("OK"sv)) { TRY(consume(" "sv)); if (consume_if("["sv)) { auto actual_type = TRY(parse_atom()); if (actual_type == "CLOSED"sv) { // No-op. } else if (actual_type == "UIDNEXT"sv) { TRY(consume(" "sv)); auto n = TRY(parse_number()); m_response.data().set_uid_next(n); } else if (actual_type == "UIDVALIDITY"sv) { TRY(consume(" "sv)); auto n = TRY(parse_number()); m_response.data().set_uid_validity(n); } else if (actual_type == "UNSEEN"sv) { TRY(consume(" "sv)); auto n = TRY(parse_number()); m_response.data().set_unseen(n); } else if (actual_type == "PERMANENTFLAGS"sv) { TRY(consume(" "sv)); auto flags = TRY(parse_list(+[](StringView x) { return ByteString(x); })); m_response.data().set_permanent_flags(move(flags)); } else if (actual_type == "HIGHESTMODSEQ"sv) { TRY(consume(" "sv)); TRY(parse_number()); // No-op for now. } else { dbgln("Unknown: {}", actual_type); consume_while([](u8 x) { return x != ']'; }); } TRY(consume("]"sv)); } consume_until_end_of_line(); TRY(consume("\r\n"sv)); } else if (consume_if("SEARCH"sv)) { Vector ids; while (!consume_if("\r\n"sv)) { TRY(consume(" "sv)); auto id = TRY(parse_number()); ids.append(id); } m_response.data().set_search_results(move(ids)); } else if (consume_if("BYE"sv)) { auto message = consume_until_end_of_line(); TRY(consume("\r\n"sv)); m_response.data().set_bye(message.is_empty() ? Optional() : Optional(message)); } else if (consume_if("STATUS"sv)) { TRY(consume(" "sv)); auto mailbox = TRY(parse_astring()); TRY(consume(" ("sv)); auto status_item = StatusItem(); status_item.set_mailbox(mailbox); while (!consume_if(")"sv)) { auto status_att = TRY(parse_atom()); TRY(consume(" "sv)); auto value = TRY(parse_number()); auto type = StatusItemType::Recent; if (status_att == "MESSAGES"sv) { type = StatusItemType::Messages; } else if (status_att == "UNSEEN"sv) { type = StatusItemType::Unseen; } else if (status_att == "UIDNEXT"sv) { type = StatusItemType::UIDNext; } else if (status_att == "UIDVALIDITY"sv) { type = StatusItemType::UIDValidity; } else if (status_att == "RECENT"sv) { type = StatusItemType::Recent; } else { dbgln("Unmatched status attribute: {}", status_att); return Error::from_string_literal("Failed to parse status attribute"); } status_item.set(type, value); if (!at_end() && m_buffer[m_position] != ')') TRY(consume(" "sv)); } m_response.data().set_status(move(status_item)); consume_if(" "sv); // Not in the spec but the Outlook server sends a space for some reason. TRY(consume("\r\n"sv)); } else { auto x = consume_until_end_of_line(); TRY(consume("\r\n"sv)); dbgln("ignored {}", x); } return {}; } ErrorOr Parser::parse_quoted_string() { dbgln_if(IMAP_PARSER_DEBUG, "p: {}, parse_quoted_string()", m_position); auto str = consume_while([](u8 x) { return x != '"'; }); TRY(consume("\""sv)); dbgln_if(IMAP_PARSER_DEBUG, "p: {}, ret \"{}\"", m_position, str); return str; } ErrorOr Parser::parse_string() { if (consume_if("\""sv)) return parse_quoted_string(); return parse_literal_string(); } ErrorOr Parser::parse_nstring() { dbgln_if(IMAP_PARSER_DEBUG, "p: {} parse_nstring()", m_position); if (consume_if("NIL"sv)) return StringView {}; return { TRY(parse_string()) }; } ErrorOr Parser::parse_fetch_response() { TRY(consume(" ("sv)); auto fetch_response = FetchResponseData(); while (!consume_if(")"sv)) { auto data_item = TRY(parse_fetch_data_item()); switch (data_item.type) { case FetchCommand::DataItemType::BodyStructure: { TRY(consume(" ("sv)); auto structure = TRY(parse_body_structure()); fetch_response.set_body_structure(move(structure)); break; } case FetchCommand::DataItemType::Envelope: { TRY(consume(" "sv)); fetch_response.set_envelope(TRY(parse_envelope())); break; } case FetchCommand::DataItemType::Flags: { TRY(consume(" "sv)); auto flags = TRY(parse_list(+[](StringView x) { return ByteString(x); })); fetch_response.set_flags(move(flags)); break; } case FetchCommand::DataItemType::InternalDate: { TRY(consume(" \""sv)); auto date_view = consume_while([](u8 x) { return x != '"'; }); TRY(consume("\""sv)); auto date = Core::DateTime::parse("%d-%b-%Y %H:%M:%S %z"sv, date_view).value(); fetch_response.set_internal_date(date); break; } case FetchCommand::DataItemType::UID: { TRY(consume(" "sv)); fetch_response.set_uid(TRY(parse_number())); break; } case FetchCommand::DataItemType::PeekBody: // Spec doesn't allow for this in a response. return Error::from_string_literal("Unexpected fetch command type"); case FetchCommand::DataItemType::BodySection: { auto body = TRY(parse_nstring()); fetch_response.add_body_data(move(data_item), body); break; } } if (!at_end() && m_buffer[m_position] != ')') TRY(consume(" "sv)); } TRY(consume("\r\n"sv)); return fetch_response; } ErrorOr Parser::parse_envelope() { TRY(consume("("sv)); auto date = TRY(parse_nstring()); TRY(consume(" "sv)); auto subject = TRY(parse_nstring()); TRY(consume(" "sv)); auto from = TRY(parse_address_list()); TRY(consume(" "sv)); auto sender = TRY(parse_address_list()); TRY(consume(" "sv)); auto reply_to = TRY(parse_address_list()); TRY(consume(" "sv)); auto to = TRY(parse_address_list()); TRY(consume(" "sv)); auto cc = TRY(parse_address_list()); TRY(consume(" "sv)); auto bcc = TRY(parse_address_list()); TRY(consume(" "sv)); auto in_reply_to = TRY(parse_nstring()); TRY(consume(" "sv)); auto message_id = TRY(parse_nstring()); TRY(consume(")"sv)); Envelope envelope = { date, subject, from, sender, reply_to, to, cc, bcc, in_reply_to, message_id }; return envelope; } ErrorOr Parser::parse_body_structure() { if (!at_end() && m_buffer[m_position] == '(') { auto data = MultiPartBodyStructureData(); while (consume_if("("sv)) { auto child = TRY(parse_body_structure()); data.bodies.append(make(move(child))); } TRY(consume(" "sv)); data.multipart_subtype = TRY(parse_string()); if (!consume_if(")"sv)) { TRY(consume(" "sv)); data.params = consume_if("NIL"sv) ? HashMap {} : TRY(parse_body_fields_params()); if (!consume_if(")"sv)) { TRY(consume(" "sv)); if (!consume_if("NIL"sv)) { data.disposition = { TRY(parse_disposition()) }; } if (!consume_if(")"sv)) { TRY(consume(" "sv)); if (!consume_if("NIL"sv)) { data.langs = { TRY(parse_langs()) }; } if (!consume_if(")"sv)) { TRY(consume(" "sv)); data.location = consume_if("NIL"sv) ? ByteString {} : ByteString(TRY(parse_string())); if (!consume_if(")"sv)) { TRY(consume(" "sv)); Vector extensions; while (!consume_if(")"sv)) { extensions.append(TRY(parse_body_extension())); consume_if(" "sv); } data.extensions = { move(extensions) }; } } } } } return BodyStructure(move(data)); } return parse_one_part_body(); } // body-type-1part ErrorOr Parser::parse_one_part_body() { // NOTE: We share common parts between body-type-basic, body-type-msg and body-type-text types for readability. BodyStructureData data; // media-basic / media-message / media-text data.type = TRY(parse_string()); TRY(consume(" "sv)); data.subtype = TRY(parse_string()); TRY(consume(" "sv)); // body-fields data.fields = TRY(parse_body_fields_params()); TRY(consume(" "sv)); data.id = TRY(parse_nstring()); TRY(consume(" "sv)); data.desc = TRY(parse_nstring()); TRY(consume(" "sv)); data.encoding = TRY(parse_string()); TRY(consume(" "sv)); data.bytes = TRY(parse_number()); if (data.type.equals_ignoring_ascii_case("TEXT"sv)) { // body-type-text // NOTE: "media-text SP body-fields" part is already parsed. TRY(consume(" "sv)); data.lines = TRY(parse_number()); } else if (data.type.equals_ignoring_ascii_case("MESSAGE"sv) && data.subtype.is_one_of_ignoring_ascii_case("RFC822"sv, "GLOBAL"sv)) { // body-type-msg // NOTE: "media-message SP body-fields" part is already parsed. TRY(consume(" "sv)); auto envelope = TRY(parse_envelope()); TRY(consume(" ("sv)); auto body = TRY(parse_body_structure()); data.contanied_message = Tuple { move(envelope), make(move(body)) }; TRY(consume(" "sv)); data.lines = TRY(parse_number()); } else { // body-type-basic // NOTE: "media-basic SP body-fields" is already parsed. } if (!consume_if(")"sv)) { TRY(consume(" "sv)); // body-ext-1part TRY([&]() -> ErrorOr { data.md5 = TRY(parse_nstring()); if (consume_if(")"sv)) return {}; TRY(consume(" "sv)); if (!consume_if("NIL"sv)) { data.disposition = { TRY(parse_disposition()) }; } if (consume_if(")"sv)) return {}; TRY(consume(" "sv)); if (!consume_if("NIL"sv)) { data.langs = { TRY(parse_langs()) }; } if (consume_if(")"sv)) return {}; TRY(consume(" "sv)); data.location = TRY(parse_nstring()); Vector extensions; while (!consume_if(")"sv)) { extensions.append(TRY(parse_body_extension())); consume_if(" "sv); } data.extensions = { move(extensions) }; return {}; }()); } return BodyStructure(move(data)); } ErrorOr> Parser::parse_langs() { AK::Vector langs; if (!consume_if("("sv)) { langs.append(TRY(parse_string())); } else { while (!consume_if(")"sv)) { langs.append(TRY(parse_string())); consume_if(" "sv); } } return langs; } ErrorOr>> Parser::parse_disposition() { TRY(consume("("sv)); auto disposition_type = TRY(parse_string()); TRY(consume(" "sv)); auto disposition_vals = TRY(parse_body_fields_params()); TRY(consume(")"sv)); return Tuple> { move(disposition_type), move(disposition_vals) }; } ErrorOr Parser::parse_literal_string() { dbgln_if(IMAP_PARSER_DEBUG, "p: {}, parse_literal_string()", m_position); TRY(consume("{"sv)); auto num_bytes = TRY(parse_number()); TRY(consume("}\r\n"sv)); if (m_buffer.size() < m_position + num_bytes) { dbgln("Attempted to parse string with length: {} at position {} (buffer length {})", num_bytes, m_position, m_position); return Error::from_string_literal("Failed to parse string"); } m_position += num_bytes; auto s = StringView(m_buffer.data() + m_position - num_bytes, num_bytes); dbgln_if(IMAP_PARSER_DEBUG, "p: {}, ret \"{}\"", m_position, s); return s; } ErrorOr Parser::parse_list_item() { TRY(consume(" "sv)); auto flags_vec = TRY(parse_list(parse_mailbox_flag)); unsigned flags = 0; for (auto flag : flags_vec) { flags |= static_cast(flag); } TRY(consume(" \""sv)); auto reference = consume_while([](u8 x) { return x != '"'; }); TRY(consume("\" "sv)); auto mailbox = TRY(parse_astring()); TRY(consume("\r\n"sv)); return ListItem { flags, ByteString(reference), ByteString(mailbox) }; } ErrorOr Parser::parse_capability_response() { auto capability = AK::Vector(); while (!consume_if("\r\n"sv)) { TRY(consume(" "sv)); capability.append(TRY(parse_atom())); } m_response.data().add_capabilities(move(capability)); return {}; } ErrorOr Parser::parse_atom() { dbgln_if(IMAP_PARSER_DEBUG, "p: {}, parse_atom()", m_position); auto is_non_atom_char = [](u8 x) { auto non_atom_chars = { '(', ')', '{', ' ', '%', '*', '"', '\\', ']' }; return AK::find(non_atom_chars.begin(), non_atom_chars.end(), x) != non_atom_chars.end(); }; auto start = m_position; auto count = 0; while (!at_end() && !is_ascii_control(m_buffer[m_position]) && !is_non_atom_char(m_buffer[m_position])) { count++; m_position++; } if (count == 0) return Error::from_string_literal("Invalid atom value"); StringView s = StringView(m_buffer.data() + start, count); dbgln_if(IMAP_PARSER_DEBUG, "p: {}, ret \"{}\"", m_position, s); return s; } ErrorOr Parser::parse_status() { auto atom = TRY(parse_atom()); if (atom == "OK"sv) { return ResponseStatus::OK; } else if (atom == "BAD"sv) { return ResponseStatus::Bad; } else if (atom == "NO"sv) { return ResponseStatus::No; } dbgln("Invalid ResponseStatus value: {}", atom); return Error::from_string_literal("Failed to parse status type"); } template ErrorOr> Parser::parse_list(T converter(StringView)) { TRY(consume("("sv)); Vector x; bool first = true; while (!consume_if(")"sv)) { if (!first) TRY(consume(" "sv)); auto item = consume_while([](u8 x) { return x != ' ' && x != ')'; }); x.append(converter(item)); first = false; } return x; } MailboxFlag Parser::parse_mailbox_flag(StringView s) { if (s == "\\All"sv) return MailboxFlag::All; if (s == "\\Drafts"sv) return MailboxFlag::Drafts; if (s == "\\Flagged"sv) return MailboxFlag::Flagged; if (s == "\\HasChildren"sv) return MailboxFlag::HasChildren; if (s == "\\HasNoChildren"sv) return MailboxFlag::HasNoChildren; if (s == "\\Important"sv) return MailboxFlag::Important; if (s == "\\Junk"sv) return MailboxFlag::Junk; if (s == "\\Marked"sv) return MailboxFlag::Marked; if (s == "\\Noinferiors"sv) return MailboxFlag::NoInferiors; if (s == "\\Noselect"sv) return MailboxFlag::NoSelect; if (s == "\\Sent"sv) return MailboxFlag::Sent; if (s == "\\Trash"sv) return MailboxFlag::Trash; if (s == "\\Unmarked"sv) return MailboxFlag::Unmarked; dbgln("Unrecognized mailbox flag {}", s); return MailboxFlag::Unknown; } StringView Parser::consume_while(Function should_consume) { dbgln_if(IMAP_PARSER_DEBUG, "p: {}, consume_while()", m_position); int chars = 0; while (!at_end() && should_consume(m_buffer[m_position])) { m_position++; chars++; } auto s = StringView(m_buffer.data() + m_position - chars, chars); dbgln_if(IMAP_PARSER_DEBUG, "p: {}, ret \"{}\"", m_position, s); return s; } StringView Parser::consume_until_end_of_line() { return consume_while([](u8 x) { return x != '\r'; }); } ErrorOr Parser::parse_fetch_data_item() { auto msg_attr = consume_while([](u8 x) { return is_ascii_alpha(x) != 0; }); if (msg_attr.equals_ignoring_ascii_case("BODY"sv) && consume_if("["sv)) { auto data_item = FetchCommand::DataItem { .type = FetchCommand::DataItemType::BodySection, .section = { {} } }; auto section_type = consume_while([](u8 x) { return x != ']' && x != ' '; }); if (section_type.equals_ignoring_ascii_case("HEADER.FIELDS"sv)) { data_item.section->type = FetchCommand::DataItem::SectionType::HeaderFields; data_item.section->headers = Vector(); TRY(consume(" "sv)); auto headers = TRY(parse_list(+[](StringView x) { return x; })); for (auto& header : headers) { data_item.section->headers->append(header); } TRY(consume("]"sv)); } else if (section_type.equals_ignoring_ascii_case("HEADER.FIELDS.NOT"sv)) { data_item.section->type = FetchCommand::DataItem::SectionType::HeaderFieldsNot; data_item.section->headers = Vector(); TRY(consume(" ("sv)); auto headers = TRY(parse_list(+[](StringView x) { return x; })); for (auto& header : headers) { data_item.section->headers->append(header); } TRY(consume("]"sv)); } else if (is_ascii_digit(section_type[0])) { data_item.section->type = FetchCommand::DataItem::SectionType::Parts; data_item.section->parts = Vector(); while (!consume_if("]"sv)) { auto num = try_parse_number(); if (num.has_value()) { data_item.section->parts->append(num.value()); continue; } auto atom = TRY(parse_atom()); if (atom.equals_ignoring_ascii_case("MIME"sv)) { data_item.section->ends_with_mime = true; continue; } } } else if (section_type.equals_ignoring_ascii_case("TEXT"sv)) { data_item.section->type = FetchCommand::DataItem::SectionType::Text; } else if (section_type.equals_ignoring_ascii_case("HEADER"sv)) { data_item.section->type = FetchCommand::DataItem::SectionType::Header; } else { dbgln("Unmatched section type {}", section_type); return Error::from_string_literal("Failed to parse section type"); } if (consume_if("<"sv)) { auto start = TRY(parse_number()); data_item.partial_fetch = true; data_item.start = (int)start; TRY(consume(">"sv)); } consume_if(" "sv); return data_item; } else if (msg_attr.equals_ignoring_ascii_case("FLAGS"sv)) { return FetchCommand::DataItem { .type = FetchCommand::DataItemType::Flags }; } else if (msg_attr.equals_ignoring_ascii_case("UID"sv)) { return FetchCommand::DataItem { .type = FetchCommand::DataItemType::UID }; } else if (msg_attr.equals_ignoring_ascii_case("INTERNALDATE"sv)) { return FetchCommand::DataItem { .type = FetchCommand::DataItemType::InternalDate }; } else if (msg_attr.equals_ignoring_ascii_case("ENVELOPE"sv)) { return FetchCommand::DataItem { .type = FetchCommand::DataItemType::Envelope }; } else if (msg_attr.equals_ignoring_ascii_case("BODY"sv) || msg_attr.equals_ignoring_ascii_case("BODYSTRUCTURE"sv)) { return FetchCommand::DataItem { .type = FetchCommand::DataItemType::BodyStructure }; } else { dbgln("msg_attr not matched: {}", msg_attr); return Error::from_string_literal("Failed to parse msg_attr"); } } ErrorOr> Parser::parse_address_list() { if (consume_if("NIL"sv)) return Vector
{}; auto addresses = Vector
(); TRY(consume("("sv)); while (!consume_if(")"sv)) { addresses.append(TRY(parse_address())); if (!at_end() && m_buffer[m_position] != ')') TRY(consume(" "sv)); } return { addresses }; } ErrorOr
Parser::parse_address() { TRY(consume("("sv)); auto address = Address(); auto name = TRY(parse_nstring()); address.name = name; TRY(consume(" "sv)); auto source_route = TRY(parse_nstring()); address.source_route = source_route; TRY(consume(" "sv)); auto mailbox = TRY(parse_nstring()); address.mailbox = mailbox; TRY(consume(" "sv)); auto host = TRY(parse_nstring()); address.host = host; TRY(consume(")"sv)); return address; } ErrorOr Parser::parse_astring() { if (!at_end() && (m_buffer[m_position] == '{' || m_buffer[m_position] == '"')) return parse_string(); return parse_atom(); } ErrorOr> Parser::parse_body_fields_params() { if (consume_if("NIL"sv)) return HashMap {}; HashMap fields; TRY(consume("("sv)); while (!consume_if(")"sv)) { auto key = TRY(parse_string()); TRY(consume(" "sv)); auto value = TRY(parse_string()); fields.set(key, value); consume_if(" "sv); } return fields; } ErrorOr Parser::parse_body_extension() { if (consume_if("NIL"sv)) return BodyExtension { Optional {} }; if (consume_if("("sv)) { Vector> extensions; while (!consume_if(")"sv)) { extensions.append(make(TRY(parse_body_extension()))); consume_if(" "sv); } return BodyExtension { move(extensions) }; } if (!at_end() && (m_buffer[m_position] == '"' || m_buffer[m_position] == '{')) return BodyExtension { { TRY(parse_string()) } }; return BodyExtension { TRY(parse_number()) }; } }