pw_base64, pw_tokenizer: Support Base64 encoding to InlineString

Fixes: b/234887297
Change-Id: I4304c360284456891ba4466fd36b42dac15b0a52
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/111471
Pigweed-Auto-Submit: Wyatt Hepler <hepler@google.com>
Reviewed-by: Alexei Frolov <frolv@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed.google.com.iam.gserviceaccount.com>
diff --git a/pw_base64/BUILD.bazel b/pw_base64/BUILD.bazel
index 20c7b0d..8a3d7ca 100644
--- a/pw_base64/BUILD.bazel
+++ b/pw_base64/BUILD.bazel
@@ -33,6 +33,7 @@
     includes = ["public"],
     deps = [
         "//pw_span",
+        "//pw_string:string",
     ],
 )
 
diff --git a/pw_base64/BUILD.gn b/pw_base64/BUILD.gn
index 40030fa..6fa1007 100644
--- a/pw_base64/BUILD.gn
+++ b/pw_base64/BUILD.gn
@@ -25,7 +25,10 @@
 pw_source_set("pw_base64") {
   public_configs = [ ":default_config" ]
   public = [ "public/pw_base64/base64.h" ]
-  public_deps = [ dir_pw_span ]
+  public_deps = [
+    "$dir_pw_string:string",
+    dir_pw_span,
+  ]
   sources = [ "base64.cc" ]
 }
 
diff --git a/pw_base64/CMakeLists.txt b/pw_base64/CMakeLists.txt
index 69f99f8..466d5e4 100644
--- a/pw_base64/CMakeLists.txt
+++ b/pw_base64/CMakeLists.txt
@@ -17,4 +17,5 @@
 pw_auto_add_simple_module(pw_base64
   PUBLIC_DEPS
     pw_span
+    pw_string.string
 )
diff --git a/pw_base64/base64.cc b/pw_base64/base64.cc
index 5184766..97f9d1e 100644
--- a/pw_base64/base64.cc
+++ b/pw_base64/base64.cc
@@ -16,6 +16,8 @@
 
 #include <cstdint>
 
+#include "pw_assert/check.h"
+
 namespace pw::base64 {
 namespace {
 
@@ -173,4 +175,16 @@
   return Decode(base64, output_buffer.data());
 }
 
+void Encode(span<const std::byte> binary, InlineString<>& output) {
+  const size_t initial_size = output.size();
+  const size_t final_size = initial_size + EncodedSize(binary.size());
+
+  PW_CHECK(final_size <= output.capacity());
+
+  output.resize_and_overwrite([&](char* data, size_t) {
+    Encode(binary, data + initial_size);
+    return final_size;
+  });
+}
+
 }  // namespace pw::base64
diff --git a/pw_base64/base64_test.cc b/pw_base64/base64_test.cc
index b503f90..a240066 100644
--- a/pw_base64/base64_test.cc
+++ b/pw_base64/base64_test.cc
@@ -232,6 +232,25 @@
   EXPECT_STREQ("aGk=", output);
 }
 
+TEST(Base64, Encode_RandomData_ToInlineString) {
+  for (const EncodedData& data : kRandomTestData) {
+    auto b64 = Encode<11>(as_bytes(span(data.binary_data, data.binary_size)));
+    EXPECT_EQ(data.encoded_data, b64);
+  }
+}
+
+TEST(Base64, Encode_RandomData_ToInlineStringAppend) {
+  for (const EncodedData& data : kRandomTestData) {
+    InlineString<32> output("Wow:");
+
+    InlineString<32> expected(output);
+    expected.append(data.encoded_data);
+
+    Encode(as_bytes(span(data.binary_data, data.binary_size)), output);
+    EXPECT_EQ(expected, output);
+  }
+}
+
 TEST(Base64, Decode_SingleChar) {
   char output[32];
   for (const EncodedData& data : kSingleCharTestData) {
@@ -267,6 +286,18 @@
   EXPECT_EQ(0, std::memcmp(expected, buf, sizeof(expected) - 1));
 }
 
+TEST(Base64, DecodeString_InPlace) {
+  constexpr const char expected[] = "This is a secret message";
+  InlineBasicString buf = "VGhpcyBpcyBhIHNlY3JldCBtZXNzYWdl";
+  DecodeInPlace(buf);
+  EXPECT_EQ(sizeof(expected) - 1, buf.size());
+  EXPECT_EQ(expected, buf);
+
+  buf.clear();
+  DecodeInPlace(buf);
+  EXPECT_TRUE(buf.empty());
+}
+
 TEST(Base64, Decode_UrlSafeDecode) {
   char output[9] = {};
 
diff --git a/pw_base64/public/pw_base64/base64.h b/pw_base64/public/pw_base64/base64.h
index 24c425e..a0dcc82 100644
--- a/pw_base64/public/pw_base64/base64.h
+++ b/pw_base64/public/pw_base64/base64.h
@@ -67,6 +67,7 @@
 #include <type_traits>
 
 #include "pw_span/span.h"
+#include "pw_string/string.h"
 
 namespace pw::base64 {
 
@@ -93,6 +94,21 @@
 // buffer is too small.
 size_t Encode(span<const std::byte> binary, span<char> output_buffer);
 
+// Appends Base64 encoded binary data to the provided pw::InlineString. If the
+// data does not fit in the string, an assertion fails.
+void Encode(span<const std::byte> binary, InlineString<>& output);
+
+// Creates a pw::InlineString<> large enough to hold kMaxBinaryDataSizeBytes of
+// binary data when encoded as Base64 and encodes the provided span into it.
+// If the data is larger than kMaxBinaryDataSizeBytes, an assertion fails.
+template <size_t kMaxBinaryDataSizeBytes>
+inline InlineString<EncodedSize(kMaxBinaryDataSizeBytes)> Encode(
+    span<const std::byte> binary) {
+  InlineString<EncodedSize(kMaxBinaryDataSizeBytes)> output;
+  Encode(binary, output);
+  return output;
+}
+
 // Returns the maximum size of decoded Base64 data in bytes. base64_size_bytes
 // must be a multiple of 4, since Base64 encodes 3-byte groups into 4-character
 // strings. If the last 3-byte group has padding, the actual decoded size would
@@ -119,6 +135,12 @@
 // invalid or doesn't fit.
 size_t Decode(std::string_view base64, span<std::byte> output_buffer);
 
+template <typename T>
+inline void DecodeInPlace(InlineBasicString<T>& buffer) {
+  static_assert(sizeof(T) == sizeof(char));
+  buffer.resize(Decode(buffer, buffer.data()));
+}
+
 // Returns true if the provided string is valid Base64 encoded data. Accepts
 // either the standard (+/) or URL-safe (-_) alphabets.
 inline bool IsValid(std::string_view base64) {
diff --git a/pw_tokenizer/BUILD.bazel b/pw_tokenizer/BUILD.bazel
index d9957c6..1c4ab26 100644
--- a/pw_tokenizer/BUILD.bazel
+++ b/pw_tokenizer/BUILD.bazel
@@ -96,6 +96,7 @@
         "//pw_base64",
         "//pw_preprocessor",
         "//pw_span",
+        "//pw_string:string",
     ],
 )
 
diff --git a/pw_tokenizer/BUILD.gn b/pw_tokenizer/BUILD.gn
index 9bff69f..46b624b 100644
--- a/pw_tokenizer/BUILD.gn
+++ b/pw_tokenizer/BUILD.gn
@@ -145,6 +145,7 @@
   sources = [ "base64.cc" ]
   public_deps = [
     ":pw_tokenizer",
+    "$dir_pw_string:string",
     dir_pw_base64,
     dir_pw_preprocessor,
   ]
diff --git a/pw_tokenizer/CMakeLists.txt b/pw_tokenizer/CMakeLists.txt
index 7928248..dcb584e 100644
--- a/pw_tokenizer/CMakeLists.txt
+++ b/pw_tokenizer/CMakeLists.txt
@@ -75,6 +75,7 @@
   PUBLIC_DEPS
     pw_base64
     pw_span
+    pw_string.string
     pw_tokenizer
     pw_tokenizer.config
   SOURCES
diff --git a/pw_tokenizer/base64.cc b/pw_tokenizer/base64.cc
index 42c48d4..658ee59 100644
--- a/pw_tokenizer/base64.cc
+++ b/pw_tokenizer/base64.cc
@@ -40,6 +40,12 @@
   return encoded_size - sizeof('\0');  // exclude the null terminator
 }
 
+void PrefixedBase64Encode(span<const std::byte> binary_message,
+                          InlineString<>& output) {
+  output.push_back(kBase64Prefix);
+  base64::Encode(binary_message, output);
+}
+
 extern "C" size_t pw_tokenizer_PrefixedBase64Decode(const void* base64_message,
                                                     size_t base64_size_bytes,
                                                     void* output_buffer,
diff --git a/pw_tokenizer/base64_test.cc b/pw_tokenizer/base64_test.cc
index 5d0019a..fee5a67 100644
--- a/pw_tokenizer/base64_test.cc
+++ b/pw_tokenizer/base64_test.cc
@@ -95,6 +95,24 @@
   EXPECT_EQ(kUnset, base64_[1]);
 }
 
+TEST_F(PrefixedBase64, Encode_InlineString) {
+  for (auto& [binary, base64] : kTestData) {
+    EXPECT_EQ(base64, PrefixedBase64Encode(binary));
+  }
+}
+
+TEST_F(PrefixedBase64, Encode_InlineString_Append) {
+  for (auto& [binary, base64] : kTestData) {
+    pw::InlineString<32> string("Other stuff!");
+    PrefixedBase64Encode(binary, string);
+
+    pw::InlineString<32> expected("Other stuff!");
+    expected.append(base64);
+
+    EXPECT_EQ(expected, string);
+  }
+}
+
 TEST_F(PrefixedBase64, Base64EncodedBufferSize_Empty_RoomForPrefixAndNull) {
   EXPECT_EQ(2u, Base64EncodedBufferSize(0));
 }
diff --git a/pw_tokenizer/docs.rst b/pw_tokenizer/docs.rst
index a4ed480..6068e6b 100644
--- a/pw_tokenizer/docs.rst
+++ b/pw_tokenizer/docs.rst
@@ -1043,12 +1043,11 @@
 .. code-block:: cpp
 
    void pw_tokenizer_HandleEncodedMessage(const uint8_t encoded_message[],
-                                         size_t size_bytes) {
-     char base64_buffer[64];
-     size_t base64_size = pw::tokenizer::PrefixedBase64Encode(
-         pw::span(encoded_message, size_bytes), base64_buffer);
+                                          size_t size_bytes) {
+     pw::InlineBasicString base64 = pw::tokenizer::PrefixedBase64Encode(
+         pw::span(encoded_message, size_bytes));
 
-     TransmitLogMessage(base64_buffer, base64_size);
+     TransmitLogMessage(base64.data(), base64.size());
    }
 
 Decoding
@@ -1074,17 +1073,6 @@
 ``pw::tokenizer::PrefixedBase64Decode`` or ``pw_tokenizer_PrefixedBase64Decode``
 functions.
 
-.. code-block:: cpp
-
-   void pw_tokenizer_HandleEncodedMessage(const uint8_t encoded_message[],
-                                         size_t size_bytes) {
-     char base64_buffer[64];
-     size_t base64_size = pw::tokenizer::PrefixedBase64Encode(
-         pw::span(encoded_message, size_bytes), base64_buffer);
-
-     TransmitLogMessage(base64_buffer, base64_size);
-   }
-
 Investigating undecoded messages
 ================================
 Tokenized messages cannot be decoded if the token is not recognized. The Python
diff --git a/pw_tokenizer/public/pw_tokenizer/base64.h b/pw_tokenizer/public/pw_tokenizer/base64.h
index 7ccc281..36acaf2 100644
--- a/pw_tokenizer/public/pw_tokenizer/base64.h
+++ b/pw_tokenizer/public/pw_tokenizer/base64.h
@@ -30,6 +30,7 @@
 #include <stddef.h>
 
 #include "pw_preprocessor/util.h"
+#include "pw_tokenizer/config.h"
 
 // This character is used to mark the start of a Base64-encoded tokenized
 // message. For consistency, it is recommended to always use $ if possible.
@@ -75,12 +76,19 @@
 
 inline constexpr char kBase64Prefix = PW_TOKENIZER_BASE64_PREFIX;
 
+#undef PW_TOKENIZER_BASE64_PREFIX  // In C++, use the variable, not the macro.
+
 // Returns the size of a tokenized message (token + arguments) when encoded as
-// prefixed Base64. This can be used to size a buffer for encoding. Includes
-// room for the prefix character ($), encoded message, and a null terminator.
+// prefixed Base64. Includes room for the prefix character ($) and encoded
+// message. This value is the capacity needed to encode to a pw::InlineString.
+constexpr size_t Base64EncodedStringSize(size_t message_size) {
+  return sizeof(kBase64Prefix) + base64::EncodedSize(message_size);
+}
+
+// Same as Base64EncodedStringSize(), but for sizing char buffers. Includes room
+// for the prefix character ($), encoded message, and a null terminator.
 constexpr size_t Base64EncodedBufferSize(size_t message_size) {
-  return sizeof(kBase64Prefix) + base64::EncodedSize(message_size) +
-         sizeof('\0');
+  return Base64EncodedStringSize(message_size) + sizeof('\0');
 }
 
 // The minimum buffer size that can hold a tokenized message that is
@@ -106,6 +114,37 @@
   return PrefixedBase64Encode(as_bytes(binary_message), output_buffer);
 }
 
+// Encodes a binary tokenized message as prefixed Base64 to a pw::InlineString,
+// appending to any existing contents. Asserts if the message does not fit in
+// the string.
+void PrefixedBase64Encode(span<const std::byte> binary_message,
+                          InlineString<>& output);
+
+inline void PrefixedBase64Encode(span<const uint8_t> binary_message,
+                                 InlineString<>& output) {
+  return PrefixedBase64Encode(as_bytes(binary_message), output);
+}
+
+// Encodes a binary tokenized message as prefixed Base64 to a pw::InlineString.
+// The pw::InlineString is sized to fit messages up to
+// kMaxBinaryMessageSizeBytes long. Asserts if the message is larger.
+template <size_t kMaxBinaryMessageSizeBytes =
+              PW_TOKENIZER_CFG_ENCODING_BUFFER_SIZE_BYTES>
+auto PrefixedBase64Encode(span<const std::byte> binary_message) {
+  static_assert(kMaxBinaryMessageSizeBytes >= 1, "Messages cannot be empty");
+  InlineString<Base64EncodedStringSize(kMaxBinaryMessageSizeBytes)> string(
+      1, kBase64Prefix);
+  base64::Encode(binary_message, string);
+  return string;
+}
+
+template <size_t kMaxBinaryMessageSizeBytes =
+              PW_TOKENIZER_CFG_ENCODING_BUFFER_SIZE_BYTES>
+auto PrefixedBase64Encode(span<const uint8_t> binary_message) {
+  return PrefixedBase64Encode<kMaxBinaryMessageSizeBytes>(
+      as_bytes(binary_message));
+}
+
 // Decodes a prefixed Base64 tokenized message to binary. Returns the size of
 // the decoded binary data. The resulting data is ready to be passed to
 // pw::tokenizer::Detokenizer::Detokenize.
@@ -124,6 +163,15 @@
       buffer.data(), buffer.size(), buffer.data(), buffer.size());
 }
 
+// Decodes a prefixed Base64 tokenized message to binary in place. Resizes the
+// string to fit the decoded binary data.
+template <typename CharT>
+inline void PrefixedBase64DecodeInPlace(InlineBasicString<CharT>& string) {
+  static_assert(sizeof(CharT) == sizeof(char));
+  string.resize(pw_tokenizer_PrefixedBase64Decode(
+      string.data(), string.size(), string.data(), string.size()));
+}
+
 }  // namespace pw::tokenizer
 
 #endif  // __cplusplus