Implement BHTTP Indeterminate-Length request encoder.
This encoder provides methods to encode an Indeterminate-Length Binary HTTP request section by section, ensuring the correct order of control data, headers, body chunks, and trailers.
PiperOrigin-RevId: 834850030
diff --git a/quiche/binary_http/binary_http_message.cc b/quiche/binary_http/binary_http_message.cc
index 6fe9542..37647c7 100644
--- a/quiche/binary_http/binary_http_message.cc
+++ b/quiche/binary_http/binary_http_message.cc
@@ -201,6 +201,44 @@
uint64_t StringPieceVarInt62Len(absl::string_view s) {
return quiche::QuicheDataWriter::GetVarInt62Len(s.length()) + s.length();
}
+
+absl::StatusOr<std::string> EncodeBodyChunksImpl(
+ absl::Span<absl::string_view> body_chunks, bool body_chunks_done) {
+ uint64_t total_length = 0;
+ for (const auto& body_chunk : body_chunks) {
+ uint8_t body_chunk_var_int_length =
+ QuicheDataWriter::GetVarInt62Len(body_chunk.size());
+ if (body_chunk_var_int_length == 0) {
+ return absl::InvalidArgumentError(
+ "Body chunk size exceeds maximum length.");
+ }
+ total_length += body_chunk_var_int_length + body_chunk.size();
+ }
+ if (body_chunks_done) {
+ total_length +=
+ quiche::QuicheDataWriter::GetVarInt62Len(kContentTerminator);
+ }
+
+ std::string data(total_length, '\0');
+ QuicheDataWriter writer(total_length, data.data());
+
+ for (const auto& body_chunk : body_chunks) {
+ if (!writer.WriteStringPieceVarInt62(body_chunk)) {
+ return absl::InternalError("Failed to write body chunk.");
+ }
+ }
+ if (body_chunks_done) {
+ if (!writer.WriteVarInt62(kContentTerminator)) {
+ return absl::InternalError("Failed to write content terminator.");
+ }
+ }
+
+ if (writer.remaining() != 0) {
+ return absl::InternalError("Failed to write all data.");
+ }
+ return data;
+}
+
} // namespace
absl::StatusOr<uint64_t> GetFieldSectionLength(
@@ -692,6 +730,122 @@
return status;
}
+absl::StatusOr<std::string>
+BinaryHttpRequest::IndeterminateLengthEncoder::EncodeFieldSection(
+ absl::Span<FieldView> fields) {
+ absl::StatusOr<uint64_t> field_section_length = GetFieldSectionLength(fields);
+ if (!field_section_length.ok()) {
+ return field_section_length.status();
+ }
+
+ std::string data(*field_section_length, '\0');
+ QuicheDataWriter writer(*field_section_length, data.data());
+
+ absl::Status fields_encoded = EncodeFields(fields, writer);
+ if (!fields_encoded.ok()) {
+ return fields_encoded;
+ }
+ if (writer.remaining() != 0) {
+ return absl::InternalError("Failed to write all fields.");
+ }
+ return data;
+}
+
+absl::StatusOr<std::string>
+BinaryHttpRequest::IndeterminateLengthEncoder::EncodeControlData(
+ const ControlData& control_data) {
+ if (current_section_ != IndeterminateLengthMessageSection::kControlData) {
+ current_section_ = IndeterminateLengthMessageSection::kEnd;
+ return absl::InvalidArgumentError(
+ "EncodeControlData called in wrong section.");
+ }
+
+ uint64_t total_length = quiche::QuicheDataWriter::GetVarInt62Len(
+ kIndeterminateLengthRequestFraming) +
+ StringPieceVarInt62Len(control_data.method) +
+ StringPieceVarInt62Len(control_data.scheme) +
+ StringPieceVarInt62Len(control_data.authority) +
+ StringPieceVarInt62Len(control_data.path);
+
+ std::string data(total_length, '\0');
+ QuicheDataWriter writer(total_length, data.data());
+ if (!writer.WriteVarInt62(kIndeterminateLengthRequestFraming)) {
+ current_section_ = IndeterminateLengthMessageSection::kEnd;
+ return absl::InternalError("Failed to write framing indicator.");
+ }
+ if (!writer.WriteStringPieceVarInt62(control_data.method)) {
+ current_section_ = IndeterminateLengthMessageSection::kEnd;
+ return absl::InternalError("Failed to write method.");
+ }
+ if (!writer.WriteStringPieceVarInt62(control_data.scheme)) {
+ current_section_ = IndeterminateLengthMessageSection::kEnd;
+ return absl::InternalError("Failed to write scheme.");
+ }
+ if (!writer.WriteStringPieceVarInt62(control_data.authority)) {
+ current_section_ = IndeterminateLengthMessageSection::kEnd;
+ return absl::InternalError("Failed to write authority.");
+ }
+ if (!writer.WriteStringPieceVarInt62(control_data.path)) {
+ current_section_ = IndeterminateLengthMessageSection::kEnd;
+ return absl::InternalError("Failed to write path.");
+ }
+ if (writer.remaining() != 0) {
+ current_section_ = IndeterminateLengthMessageSection::kEnd;
+ return absl::InternalError("Failed to write all control data.");
+ }
+
+ current_section_ = IndeterminateLengthMessageSection::kHeader;
+ return data;
+}
+
+absl::StatusOr<std::string>
+BinaryHttpRequest::IndeterminateLengthEncoder::EncodeHeaders(
+ absl::Span<FieldView> headers) {
+ if (current_section_ != IndeterminateLengthMessageSection::kHeader) {
+ current_section_ = IndeterminateLengthMessageSection::kEnd;
+ return absl::InvalidArgumentError("EncodeHeaders called in wrong section.");
+ }
+ absl::StatusOr<std::string> data = EncodeFieldSection(headers);
+ if (!data.ok()) {
+ current_section_ = IndeterminateLengthMessageSection::kEnd;
+ return data;
+ }
+ current_section_ = IndeterminateLengthMessageSection::kBody;
+ return data;
+}
+
+absl::StatusOr<std::string>
+BinaryHttpRequest::IndeterminateLengthEncoder::EncodeBodyChunks(
+ absl::Span<absl::string_view> body_chunks, bool body_chunks_done) {
+ if (current_section_ != IndeterminateLengthMessageSection::kBody) {
+ current_section_ = IndeterminateLengthMessageSection::kEnd;
+ return absl::InvalidArgumentError(
+ "EncodeBodyChunks called in wrong section.");
+ }
+ absl::StatusOr<std::string> result =
+ EncodeBodyChunksImpl(body_chunks, body_chunks_done);
+ if (!result.ok()) {
+ current_section_ = IndeterminateLengthMessageSection::kEnd;
+ return result.status();
+ }
+ if (body_chunks_done) {
+ current_section_ = IndeterminateLengthMessageSection::kTrailer;
+ }
+ return result;
+}
+
+absl::StatusOr<std::string>
+BinaryHttpRequest::IndeterminateLengthEncoder::EncodeTrailers(
+ absl::Span<FieldView> trailers) {
+ if (current_section_ != IndeterminateLengthMessageSection::kTrailer) {
+ current_section_ = IndeterminateLengthMessageSection::kEnd;
+ return absl::InvalidArgumentError(
+ "EncodeTrailers called in wrong section.");
+ }
+ current_section_ = IndeterminateLengthMessageSection::kEnd;
+ return EncodeFieldSection(trailers);
+}
+
absl::StatusOr<BinaryHttpResponse> BinaryHttpResponse::Create(
absl::string_view data) {
quiche::QuicheDataReader reader(data);
@@ -819,44 +973,16 @@
absl::StrCat("EncodeBodyChunks called in incorrect section: ",
GetMessageSectionString(current_section_)));
}
- uint64_t total_length = 0;
- for (const auto& body_chunk : body_chunks) {
- uint8_t body_chunk_var_int_length =
- QuicheDataWriter::GetVarInt62Len(body_chunk.size());
- if (body_chunk_var_int_length == 0) {
- current_section_ = IndeterminateLengthMessageSection::kEnd;
- return absl::InvalidArgumentError(
- "Body chunk size exceeds maximum length.");
- }
- total_length += body_chunk_var_int_length + body_chunk.size();
+ absl::StatusOr<std::string> result =
+ EncodeBodyChunksImpl(body_chunks, body_chunks_done);
+ if (!result.ok()) {
+ current_section_ = IndeterminateLengthMessageSection::kEnd;
+ return result.status();
}
if (body_chunks_done) {
- total_length +=
- quiche::QuicheDataWriter::GetVarInt62Len(kContentTerminator);
- }
-
- std::string data(total_length, '\0');
- QuicheDataWriter writer(total_length, data.data());
-
- for (const auto& body_chunk : body_chunks) {
- if (!writer.WriteStringPieceVarInt62(body_chunk)) {
- current_section_ = IndeterminateLengthMessageSection::kEnd;
- return absl::InternalError("Failed to write body chunk.");
- }
- }
- if (body_chunks_done) {
- if (!writer.WriteVarInt62(kContentTerminator)) {
- current_section_ = IndeterminateLengthMessageSection::kEnd;
- return absl::InternalError("Failed to write content terminator.");
- }
current_section_ = IndeterminateLengthMessageSection::kTrailer;
}
-
- if (writer.remaining() != 0) {
- current_section_ = IndeterminateLengthMessageSection::kEnd;
- return absl::InternalError("Failed to write all data.");
- }
- return data;
+ return result;
}
absl::StatusOr<std::string>
diff --git a/quiche/binary_http/binary_http_message.h b/quiche/binary_http/binary_http_message.h
index ecadfb7..be23ee0 100644
--- a/quiche/binary_http/binary_http_message.h
+++ b/quiche/binary_http/binary_http_message.h
@@ -186,6 +186,12 @@
// its corresponding section is successfully decoded.
class QUICHE_EXPORT IndeterminateLengthDecoder;
+ // Provides encoding methods for an Indeterminate-Length BHTTP request. The
+ // encoder keeps track of what has been encoded so far to ensure sections are
+ // encoded in the correct order, this means it can only be used for a single
+ // request message.
+ class QUICHE_EXPORT IndeterminateLengthEncoder;
+
private:
// The sections of an Indeterminate-Length BHTTP request.
enum class IndeterminateLengthMessageSection {
@@ -284,6 +290,34 @@
bool maybe_truncated_ = true;
};
+// Provides encoding methods for an Indeterminate-Length BHTTP request. The
+// encoder keeps track of what has been encoded so far to ensure sections are
+// encoded in the correct order, this means it can only be used for a single
+// request message.
+class QUICHE_EXPORT BinaryHttpRequest::IndeterminateLengthEncoder {
+ public:
+ // Encodes the initial framing indicator and the specified control data.
+ absl::StatusOr<std::string> EncodeControlData(
+ const ControlData& control_data);
+ // Encodes the specified headers and its content terminator.
+ absl::StatusOr<std::string> EncodeHeaders(absl::Span<FieldView> headers);
+ // Encodes the specified body chunks. This can be called multiple times but
+ // it needs to be called exactly once with `body_chunks_done` set to true at
+ // the end to properly set the content terminator. Encoding body chunks is
+ // optional since valid chunked messages can be truncated.
+ absl::StatusOr<std::string> EncodeBodyChunks(
+ absl::Span<absl::string_view> body_chunks, bool body_chunks_done);
+ // Encodes the specified trailers and its content terminator. Encoding
+ // trailers is optional since valid chunked messages can be truncated.
+ absl::StatusOr<std::string> EncodeTrailers(absl::Span<FieldView> trailers);
+
+ private:
+ absl::StatusOr<std::string> EncodeFieldSection(absl::Span<FieldView> fields);
+
+ IndeterminateLengthMessageSection current_section_ =
+ IndeterminateLengthMessageSection::kControlData;
+};
+
void QUICHE_EXPORT PrintTo(const BinaryHttpRequest& msg, std::ostream* os);
class QUICHE_EXPORT BinaryHttpResponse : public BinaryHttpMessage {
@@ -412,11 +446,14 @@
// Encodes the specified status code, headers, and its content terminator.
absl::StatusOr<std::string> EncodeHeaders(uint16_t status_code,
absl::Span<FieldView> headers);
- // Encodes the specified body chunks. If 'body_chunks_done' is true, the
- // encoded body chunks are followed by the content terminator.
+ // Encodes the specified body chunks. This can be called multiple times but
+ // it needs to be called exactly once with `body_chunks_done` set to true at
+ // the end to properly set the content terminator. Encoding body chunks is
+ // optional since valid chunked messages can be truncated.
absl::StatusOr<std::string> EncodeBodyChunks(
absl::Span<absl::string_view> body_chunks, bool body_chunks_done);
- // Encodes the specified trailers and its content terminator.
+ // Encodes the specified trailers and its content terminator. Encoding
+ // trailers is optional since valid chunked messages can be truncated.
absl::StatusOr<std::string> EncodeTrailers(absl::Span<FieldView> trailers);
private:
diff --git a/quiche/binary_http/binary_http_message_test.cc b/quiche/binary_http/binary_http_message_test.cc
index ac8ffb1..489474d 100644
--- a/quiche/binary_http/binary_http_message_test.cc
+++ b/quiche/binary_http/binary_http_message_test.cc
@@ -8,7 +8,6 @@
#include <utility>
#include <vector>
-#include "absl/container/flat_hash_map.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/escaping.h"
@@ -96,6 +95,8 @@
MessageData message_data_;
};
+constexpr absl::string_view kFramingIndicator =
+ "02"; // 1-byte framing indicator
constexpr absl::string_view k2ByteFramingIndicator =
"4002"; // 2-byte framing indicator
constexpr absl::string_view k8ByteContentTerminator =
@@ -752,6 +753,231 @@
namespace {
+struct RequestIndeterminateLengthEncoderTestData {
+ BinaryHttpRequest::ControlData control_data{"POST", "https", "google.com",
+ "/hello"};
+ std::vector<BinaryHttpMessage::FieldView> headers{
+ {"User-Agent", "curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3"},
+ {"accept-language", "en, mi"}};
+ std::vector<absl::string_view> body_chunks = {"chunk1", "chunk2", "chunk3"};
+ std::vector<BinaryHttpMessage::FieldView> trailers{{"trailer1", "value1"},
+ {"trailer2", "value2"}};
+};
+
+} // namespace
+
+TEST(RequestIndeterminateLengthEncoder, FullRequest) {
+ std::string expected;
+ ASSERT_TRUE(absl::HexStringToBytes(
+ absl::StrCat(
+ kFramingIndicator, kIndeterminateLengthEncodedRequestControlData,
+ kIndeterminateLengthEncodedRequestHeaders, kContentTerminator,
+ kIndeterminateLengthEncodedRequestBodyChunks, kContentTerminator,
+ kIndeterminateLengthEncodedRequestTrailers, kContentTerminator),
+ &expected));
+
+ BinaryHttpRequest::IndeterminateLengthEncoder encoder;
+ RequestIndeterminateLengthEncoderTestData test_data;
+ std::string encoded_data;
+
+ absl::StatusOr<std::string> status_or_encoded_data =
+ encoder.EncodeControlData(test_data.control_data);
+ QUICHE_EXPECT_OK(status_or_encoded_data);
+ if (status_or_encoded_data.ok()) {
+ encoded_data += *status_or_encoded_data;
+ }
+
+ status_or_encoded_data =
+ encoder.EncodeHeaders(absl::MakeSpan(test_data.headers));
+ QUICHE_EXPECT_OK(status_or_encoded_data);
+ if (status_or_encoded_data.ok()) {
+ encoded_data += *status_or_encoded_data;
+ }
+
+ status_or_encoded_data =
+ encoder.EncodeBodyChunks(absl::MakeSpan(test_data.body_chunks), true);
+ QUICHE_EXPECT_OK(status_or_encoded_data);
+ if (status_or_encoded_data.ok()) {
+ encoded_data += *status_or_encoded_data;
+ }
+
+ status_or_encoded_data =
+ encoder.EncodeTrailers(absl::MakeSpan(test_data.trailers));
+ QUICHE_EXPECT_OK(status_or_encoded_data);
+ if (status_or_encoded_data.ok()) {
+ encoded_data += *status_or_encoded_data;
+ }
+
+ EXPECT_EQ(encoded_data, expected);
+}
+
+TEST(RequestIndeterminateLengthEncoder, RequestNoBody) {
+ std::string expected;
+ ASSERT_TRUE(absl::HexStringToBytes(
+ absl::StrCat(
+ kFramingIndicator, kIndeterminateLengthEncodedRequestControlData,
+ kIndeterminateLengthEncodedRequestHeaders, kContentTerminator,
+ kContentTerminator, // Empty body chunks
+ kContentTerminator), // Empty trailers
+ &expected));
+
+ BinaryHttpRequest::IndeterminateLengthEncoder encoder;
+ RequestIndeterminateLengthEncoderTestData test_data;
+ std::string encoded_data;
+
+ absl::StatusOr<std::string> status_or_encoded_data =
+ encoder.EncodeControlData(test_data.control_data);
+ QUICHE_EXPECT_OK(status_or_encoded_data);
+ if (status_or_encoded_data.ok()) {
+ encoded_data += *status_or_encoded_data;
+ }
+
+ status_or_encoded_data =
+ encoder.EncodeHeaders(absl::MakeSpan(test_data.headers));
+ QUICHE_EXPECT_OK(status_or_encoded_data);
+ if (status_or_encoded_data.ok()) {
+ encoded_data += *status_or_encoded_data;
+ }
+
+ status_or_encoded_data = encoder.EncodeBodyChunks({}, true);
+ QUICHE_EXPECT_OK(status_or_encoded_data);
+ if (status_or_encoded_data.ok()) {
+ encoded_data += *status_or_encoded_data;
+ }
+
+ status_or_encoded_data = encoder.EncodeTrailers({});
+ QUICHE_EXPECT_OK(status_or_encoded_data);
+ if (status_or_encoded_data.ok()) {
+ encoded_data += *status_or_encoded_data;
+ }
+
+ EXPECT_EQ(encoded_data, expected);
+}
+
+TEST(RequestIndeterminateLengthEncoder, EncodingChunksMultipleTimes) {
+ std::string expected;
+ ASSERT_TRUE(absl::HexStringToBytes(
+ absl::StrCat(
+ kFramingIndicator, kIndeterminateLengthEncodedRequestControlData,
+ kIndeterminateLengthEncodedRequestHeaders, kContentTerminator,
+ kIndeterminateLengthEncodedRequestBodyChunks, kContentTerminator,
+ kIndeterminateLengthEncodedRequestTrailers, kContentTerminator),
+ &expected));
+
+ BinaryHttpRequest::IndeterminateLengthEncoder encoder;
+ RequestIndeterminateLengthEncoderTestData test_data;
+ std::string encoded_data;
+
+ absl::StatusOr<std::string> status_or_encoded_data =
+ encoder.EncodeControlData(test_data.control_data);
+ QUICHE_EXPECT_OK(status_or_encoded_data);
+ if (status_or_encoded_data.ok()) {
+ encoded_data += *status_or_encoded_data;
+ }
+
+ status_or_encoded_data =
+ encoder.EncodeHeaders(absl::MakeSpan(test_data.headers));
+ QUICHE_EXPECT_OK(status_or_encoded_data);
+ if (status_or_encoded_data.ok()) {
+ encoded_data += *status_or_encoded_data;
+ }
+
+ status_or_encoded_data = encoder.EncodeBodyChunks(
+ absl::MakeSpan(test_data.body_chunks.data(), 1), false);
+ QUICHE_EXPECT_OK(status_or_encoded_data);
+ if (status_or_encoded_data.ok()) {
+ encoded_data += *status_or_encoded_data;
+ }
+ status_or_encoded_data = encoder.EncodeBodyChunks(
+ absl::MakeSpan(test_data.body_chunks.data() + 1, 1), false);
+ QUICHE_EXPECT_OK(status_or_encoded_data);
+ if (status_or_encoded_data.ok()) {
+ encoded_data += *status_or_encoded_data;
+ }
+ status_or_encoded_data = encoder.EncodeBodyChunks(
+ absl::MakeSpan(test_data.body_chunks.data() + 2, 1), false);
+ QUICHE_EXPECT_OK(status_or_encoded_data);
+ if (status_or_encoded_data.ok()) {
+ encoded_data += *status_or_encoded_data;
+ }
+ status_or_encoded_data = encoder.EncodeBodyChunks({}, true);
+ QUICHE_EXPECT_OK(status_or_encoded_data);
+ if (status_or_encoded_data.ok()) {
+ encoded_data += *status_or_encoded_data;
+ }
+
+ status_or_encoded_data =
+ encoder.EncodeTrailers(absl::MakeSpan(test_data.trailers));
+ QUICHE_EXPECT_OK(status_or_encoded_data);
+ if (status_or_encoded_data.ok()) {
+ encoded_data += *status_or_encoded_data;
+ }
+ EXPECT_EQ(encoded_data, expected);
+
+ RequestMessageSectionTestHandler handler;
+ BinaryHttpRequest::IndeterminateLengthDecoder decoder(handler);
+ QUICHE_EXPECT_OK(decoder.Decode(encoded_data, true));
+ ExpectRequestMessageSectionHandler(handler.GetMessageData());
+}
+
+TEST(RequestIndeterminateLengthEncoder, OutOfOrderHeaders) {
+ BinaryHttpRequest::IndeterminateLengthEncoder encoder;
+ EXPECT_THAT(encoder.EncodeHeaders({}),
+ test::StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(RequestIndeterminateLengthEncoder, OutOfOrderBodyChunks) {
+ BinaryHttpRequest::IndeterminateLengthEncoder encoder;
+ EXPECT_THAT(encoder.EncodeBodyChunks({}, true),
+ test::StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(RequestIndeterminateLengthEncoder, OutOfOrderTrailers) {
+ BinaryHttpRequest::IndeterminateLengthEncoder encoder;
+ EXPECT_THAT(encoder.EncodeTrailers({}),
+ test::StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(RequestIndeterminateLengthEncoder, MustNotEncodeControlDataTwice) {
+ BinaryHttpRequest::IndeterminateLengthEncoder encoder;
+ RequestIndeterminateLengthEncoderTestData test_data;
+ QUICHE_EXPECT_OK(encoder.EncodeControlData(test_data.control_data));
+ EXPECT_THAT(encoder.EncodeControlData(test_data.control_data),
+ test::StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(RequestIndeterminateLengthEncoder, MustNotEncodeHeadersTwice) {
+ BinaryHttpRequest::IndeterminateLengthEncoder encoder;
+ RequestIndeterminateLengthEncoderTestData test_data;
+ QUICHE_EXPECT_OK(encoder.EncodeControlData(test_data.control_data));
+ QUICHE_EXPECT_OK(encoder.EncodeHeaders({}));
+ EXPECT_THAT(encoder.EncodeHeaders({}),
+ test::StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(RequestIndeterminateLengthEncoder, MustNotEncodeChunksAfterChunksDone) {
+ BinaryHttpRequest::IndeterminateLengthEncoder encoder;
+ RequestIndeterminateLengthEncoderTestData test_data;
+ QUICHE_EXPECT_OK(encoder.EncodeControlData(test_data.control_data));
+ QUICHE_EXPECT_OK(encoder.EncodeHeaders({}));
+ QUICHE_EXPECT_OK(encoder.EncodeBodyChunks({}, true));
+ EXPECT_THAT(encoder.EncodeBodyChunks({}, true),
+ test::StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(RequestIndeterminateLengthEncoder, MustNotEncodeTrailersTwice) {
+ BinaryHttpRequest::IndeterminateLengthEncoder encoder;
+ RequestIndeterminateLengthEncoderTestData test_data;
+ QUICHE_EXPECT_OK(encoder.EncodeControlData(test_data.control_data));
+ QUICHE_EXPECT_OK(encoder.EncodeHeaders({}));
+ QUICHE_EXPECT_OK(encoder.EncodeBodyChunks({}, true));
+ QUICHE_EXPECT_OK(encoder.EncodeTrailers({}));
+ EXPECT_THAT(encoder.EncodeTrailers({}),
+ test::StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+namespace {
+
struct ResponseIndeterminateLengthEncoderTestData {
std::vector<quiche::BinaryHttpMessage::FieldView> informationalResponse1{
{"running", "\"sleep 15\""}};