pw_protobuf: Add helper for map entry write
Expose low level APIs in pw_protobuf for estimating field size and
writing length-delimited field key and length prefix. Use them to
implement a helper function in pw_software_update for writing proto
map<string, bytes> entries.
Context: UpdateBundle needs to construct the following Manifest
proto message from blob storage:
message Manifest {
optional SnapshotMetadata snapshot_metadata = 1;
map<string, TargetsMetadata> targets_metadata = 2;
}
For wire format generation, it is essentially equivalent to the
following definition with a nested `Entry` message.
message Entry {
string key = 1;
bytes value = 2;
}
message Manifest {
optional bytes snapshot_metadata = 1;
repeated Entry targets_metadata = 2;
}
Although protobuf::StreamEncoder has capability for nested message
encoding, it requires a scratch buffer that shall be at least the
largest sub-message size. In this case, it will be largest
target metadata in the update software bundle, which however, can be
fairly large and difficult to estimate. To avoid the issue, the CL
takes an approach to construct the message from lower level.
Specifically, the CL constructs the delimited field key and length
prefix for `Entry` on its own and write to output, then followed by
writing a regular string field of `key` and bytes field of `value` via
the normal StreamEncoder approach.
Change-Id: Ie5f10f483ebceb587660a4a36e5c6674a09ce096
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/59681
Reviewed-by: Armando Montanez <amontanez@google.com>
Commit-Queue: Yecheng Zhao <zyecheng@google.com>
diff --git a/pw_protobuf/helpers_test.cc b/pw_protobuf/helpers_test.cc
new file mode 100644
index 0000000..6d2f8cf
--- /dev/null
+++ b/pw_protobuf/helpers_test.cc
@@ -0,0 +1,152 @@
+// Copyright 2021 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_protobuf/helpers.h"
+
+#include "gtest/gtest.h"
+#include "pw_protobuf/encoder.h"
+
+namespace pw::protobuf {
+
+TEST(ProtoHelper, WriteProtoStringToBytesMapEntry) {
+ // The following defines an instance of the message below:
+ //
+ // message Maps {
+ // map<string, string> map_a = 1;
+ // map<string, string> map_b = 2;
+ // }
+ //
+ // where
+ //
+ // Maps.map_a['key_foo'] = 'foo_a'
+ // Maps.map_a['key_bar'] = 'bar_a'
+ //
+ // Maps.map_b['key_foo'] = 'foo_b'
+ // Maps.map_b['key_bar'] = 'bar_b'
+ //
+ // clang-format off
+ std::uint8_t encoded_proto[] = {
+ // map_a["key_bar"] = "bar_a", key = 1
+ 0x0a, 0x10,
+ 0x0a, 0x07, 'k', 'e', 'y', '_', 'b', 'a', 'r', // map key
+ 0x12, 0x05, 'b', 'a', 'r', '_', 'a', // map value
+
+ // map_a["key_foo"] = "foo_a", key = 1
+ 0x0a, 0x10,
+ 0x0a, 0x07, 'k', 'e', 'y', '_', 'f', 'o', 'o',
+ 0x12, 0x05, 'f', 'o', 'o', '_', 'a',
+
+ // map_b["key_foo"] = "foo_b", key = 2
+ 0x12, 0x10,
+ 0x0a, 0x07, 'k', 'e', 'y', '_', 'f', 'o', 'o',
+ 0x12, 0x05, 'f', 'o', 'o', '_', 'b',
+
+ // map_b["key_bar"] = "bar_b", key = 2
+ 0x12, 0x10,
+ 0x0a, 0x07, 'k', 'e', 'y', '_', 'b', 'a', 'r',
+ 0x12, 0x05, 'b', 'a', 'r', '_', 'b',
+ };
+ // clang-format on
+
+ // Now construct the same message with WriteStringToBytesMapEntry
+ std::byte dst_buffer[sizeof(encoded_proto)];
+ stream::MemoryWriter writer(dst_buffer);
+
+ const struct {
+ uint32_t field_number;
+ std::string_view key;
+ std::string_view value;
+ } kMapData[] = {
+ {1, "key_bar", "bar_a"},
+ {1, "key_foo", "foo_a"},
+ {2, "key_foo", "foo_b"},
+ {2, "key_bar", "bar_b"},
+ };
+
+ std::byte stream_pipe_buffer[1];
+ for (auto ele : kMapData) {
+ stream::MemoryReader key_reader(std::as_bytes(std::span{ele.key}));
+ stream::MemoryReader value_reader(std::as_bytes(std::span{ele.value}));
+ ASSERT_TRUE(WriteProtoStringToBytesMapEntry(ele.field_number,
+ key_reader,
+ ele.key.size(),
+ value_reader,
+ ele.value.size(),
+ stream_pipe_buffer,
+ writer)
+ .ok());
+ }
+
+ ASSERT_EQ(memcmp(dst_buffer, encoded_proto, sizeof(dst_buffer)), 0);
+}
+
+TEST(ProtoHelper, WriteProtoStringToBytesMapEntryExceedsWriteLimit) {
+ // Construct an instance of the message below:
+ //
+ // message Maps {
+ // map<string, string> map_a = 1;
+ // }
+ //
+ // where
+ //
+ // Maps.map_a['key_bar'] = 'bar_a'. The needed buffer size is 18 in this
+ // case:
+ //
+ // {
+ // 0x0a, 0x10,
+ // 0x0a, 0x07, 'k', 'e', 'y', '_', 'b', 'a', 'r',
+ // 0x12, 0x05, 'b', 'a', 'r', '_', 'a',
+ // }
+ //
+ // Use a smaller buffer.
+ std::byte encode_buffer[17];
+ stream::MemoryWriter writer(encode_buffer);
+ constexpr uint32_t kFieldNumber = 1;
+ std::string_view key = "key_bar";
+ std::string_view value = "bar_a";
+ stream::MemoryReader key_reader(std::as_bytes(std::span{key}));
+ stream::MemoryReader value_reader(std::as_bytes(std::span{value}));
+ std::byte stream_pipe_buffer[1];
+ ASSERT_EQ(
+ WriteProtoStringToBytesMapEntry(kFieldNumber,
+ key_reader,
+ key_reader.ConservativeReadLimit(),
+ value_reader,
+ value_reader.ConservativeReadLimit(),
+ stream_pipe_buffer,
+ writer),
+ Status::ResourceExhausted());
+}
+
+TEST(ProtoHelper, WriteProtoStringToBytesMapEntryInvalidArgument) {
+ std::byte encode_buffer[17];
+ stream::MemoryWriter writer(encode_buffer);
+ std::string_view key = "key_bar";
+ std::string_view value = "bar_a";
+ stream::MemoryReader key_reader(std::as_bytes(std::span{key}));
+ stream::MemoryReader value_reader(std::as_bytes(std::span{value}));
+ std::byte stream_pipe_buffer[1];
+
+ ASSERT_EQ(
+ WriteProtoStringToBytesMapEntry(19091,
+ key_reader,
+ key_reader.ConservativeReadLimit(),
+ value_reader,
+ value_reader.ConservativeReadLimit(),
+ stream_pipe_buffer,
+ writer),
+ Status::InvalidArgument());
+}
+
+} // namespace pw::protobuf