pw_string: Copy to and from pw::Vector

pw_protobuf uses pw::Vector<char> to hold a variable length string with
a size.

Add variants of pw::string::Copy() for writing to and from this type.

Change-Id: I9a71ef3c171404c62c485a97e5b92678723d6952
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/89942
Pigweed-Auto-Submit: Scott James Remnant <keybuk@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed.google.com.iam.gserviceaccount.com>
diff --git a/pw_containers/BUILD.bazel b/pw_containers/BUILD.bazel
index 6928f09..0d8b1ae 100644
--- a/pw_containers/BUILD.bazel
+++ b/pw_containers/BUILD.bazel
@@ -51,7 +51,7 @@
     ],
     includes = ["public"],
     deps = [
-        "//pw_assert",
+        "//pw_assert:facade",
         "//pw_polyfill",
     ],
 )
diff --git a/pw_string/BUILD.bazel b/pw_string/BUILD.bazel
index 663bb4f..6d2f22c 100644
--- a/pw_string/BUILD.bazel
+++ b/pw_string/BUILD.bazel
@@ -47,6 +47,19 @@
     ],
 )
 
+pw_cc_library(
+    name = "vector",
+    hdrs = [
+        "public/pw_string/vector.h",
+    ],
+    includes = ["public"],
+    deps = [
+        ":pw_string",
+        "//pw_containers:vector",
+        "//pw_status",
+    ],
+)
+
 pw_cc_test(
     name = "format_test",
     srcs = ["format_test.cc"],
@@ -93,3 +106,13 @@
         "//pw_unit_test",
     ],
 )
+
+pw_cc_test(
+    name = "vector_test",
+    srcs = ["vector_test.cc"],
+    deps = [
+        ":vector",
+        "//pw_containers:vector",
+        "//pw_unit_test",
+    ],
+)
diff --git a/pw_string/BUILD.gn b/pw_string/BUILD.gn
index 0742938..633f3b1 100644
--- a/pw_string/BUILD.gn
+++ b/pw_string/BUILD.gn
@@ -46,6 +46,16 @@
   ]
 }
 
+pw_source_set("vector") {
+  public_configs = [ ":public_include_path" ]
+  public = [ "public/pw_string/vector.h" ]
+  public_deps = [
+    ":pw_string",
+    "$dir_pw_containers:vector",
+    "$dir_pw_status",
+  ]
+}
+
 pw_test_group("tests") {
   tests = [
     ":format_test",
@@ -53,6 +63,7 @@
     ":to_string_test",
     ":type_to_string_test",
     ":util_test",
+    ":vector_test",
   ]
   group_deps = [
     "$dir_pw_preprocessor:tests",
@@ -85,6 +96,14 @@
   sources = [ "util_test.cc" ]
 }
 
+pw_test("vector_test") {
+  deps = [
+    ":vector",
+    "$dir_pw_containers:vector",
+  ]
+  sources = [ "vector_test.cc" ]
+}
+
 pw_doc_group("docs") {
   sources = [ "docs.rst" ]
   report_deps = [
diff --git a/pw_string/CMakeLists.txt b/pw_string/CMakeLists.txt
index 6103ddc..e98a132 100644
--- a/pw_string/CMakeLists.txt
+++ b/pw_string/CMakeLists.txt
@@ -26,6 +26,7 @@
     public
   PUBLIC_DEPS
     pw_assert
+    pw_containers.vector
     pw_polyfill.span
     pw_preprocessor
     pw_result
@@ -39,6 +40,17 @@
   zephyr_link_libraries(pw_string)
 endif()
 
+pw_add_module_library(pw_string.vector
+  HEADERS
+    public/pw_string/vector.h
+  PUBLIC_INCLUDES
+    public
+  PUBLIC_DEPS
+    pw_containers.vector
+    pw_status
+    pw_string
+)
+
 pw_add_test(pw_string.format_test
   SOURCES
     format_test.cc
@@ -88,3 +100,15 @@
     modules
     pw_string
 )
+
+pw_add_test(pw_string.vector_test
+  SOURCES
+    vector_test.cc
+  DEPS
+    pw_containers.vector
+    pw_string.vector
+  GROUPS
+    modules
+    pw_string
+)
+
diff --git a/pw_string/docs.rst b/pw_string/docs.rst
index c44f580..6b8b77e 100644
--- a/pw_string/docs.rst
+++ b/pw_string/docs.rst
@@ -83,6 +83,7 @@
 .. cpp:function:: StatusWithSize Copy(const std::string_view& source, std::span<char> dest)
 .. cpp:function:: StatusWithSize Copy(const char* source, std::span<char> dest)
 .. cpp:function:: StatusWithSize Copy(const char* source, char* dest, size_t num)
+.. cpp:function:: StatusWithSize Copy(const pw::Vector<char>& source, std::span<char> dest)
 
    Copies the source string to the dest, truncating if the full string does not
    fit. Always null terminates if dest.size() or num > 0.
@@ -93,6 +94,13 @@
    Precondition: The destination and source shall not overlap.
    Precondition: The source shall be a valid pointer.
 
+It also has variants that provide a destination of ``pw::Vector<char>``
+(see :ref:`module-pw_containers` for details) that do not store the null
+terminator in the vector.
+
+.. cpp:function:: StatusWithSize Copy(const std::string_view& source, pw::Vector<char>& dest)
+.. cpp:function:: StatusWithSize Copy(const char* source, pw::Vector<char>& dest)
+
 pw::StringBuilder
 =================
 ``pw::StringBuilder`` facilitates building formatted strings in a fixed-size
diff --git a/pw_string/public/pw_string/vector.h b/pw_string/public/pw_string/vector.h
new file mode 100644
index 0000000..5cdc019
--- /dev/null
+++ b/pw_string/public/pw_string/vector.h
@@ -0,0 +1,72 @@
+// Copyright 2022 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.
+#pragma once
+
+#include <cstddef>
+#include <span>
+#include <string_view>
+
+#include "pw_containers/vector.h"
+#include "pw_status/status.h"
+#include "pw_status/status_with_size.h"
+#include "pw_string/util.h"
+
+namespace pw {
+namespace string {
+
+// Copies the source string to the dest, truncating if the full string does not
+// fit. dest will not be null terminated, instead the length is reflected in
+// the vector size.
+//
+// Returns the number of characters written. If the string is truncated, the
+// status is ResourceExhausted.
+//
+// Precondition: The destination and source shall not overlap.
+// Precondition: The source shall be a valid pointer.
+inline StatusWithSize Copy(const std::string_view& source,
+                           pw::Vector<char>& dest) {
+  if (dest.capacity() == 0) {
+    return StatusWithSize::ResourceExhausted();
+  }
+
+  dest.resize(source.size());
+  const size_t copied = source.copy(dest.data(), dest.size());
+
+  return StatusWithSize(
+      copied == source.size() ? OkStatus() : Status::ResourceExhausted(),
+      copied);
+}
+
+inline StatusWithSize Copy(const char* source, pw::Vector<char>& dest) {
+  PW_DASSERT(source != nullptr);
+  return Copy(ClampedCString(source, dest.capacity()), dest);
+}
+
+inline StatusWithSize Copy(const pw::Vector<char>& source,
+                           std::span<char> dest) {
+  if (dest.empty()) {
+    return StatusWithSize::ResourceExhausted();
+  }
+
+  const size_t copied = std::string_view(source.data(), source.size())
+                            .copy(dest.data(), dest.size() - 1);
+  dest[copied] = '\0';
+
+  return StatusWithSize(
+      copied == source.size() ? OkStatus() : Status::ResourceExhausted(),
+      copied);
+}
+
+}  // namespace string
+}  // namespace pw
diff --git a/pw_string/vector_test.cc b/pw_string/vector_test.cc
new file mode 100644
index 0000000..89e049c
--- /dev/null
+++ b/pw_string/vector_test.cc
@@ -0,0 +1,112 @@
+// Copyright 2022 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_string/vector.h"
+
+#include <string>
+#include <string_view>
+
+#include "gtest/gtest.h"
+
+namespace pw::string {
+namespace {
+
+using namespace std::literals::string_view_literals;
+
+TEST(CopyIntoVectorTest, EmptyStringView) {
+  pw::Vector<char, 32> vector{};
+  EXPECT_EQ(0u, Copy("", vector).size());
+  EXPECT_EQ(0u, vector.size());
+}
+
+TEST(CopyIntoVectorTest, EmptyVector_WritesNothing) {
+  pw::Vector<char, 0> vector{};
+  auto result = Copy("Hello", vector);
+  EXPECT_EQ(0u, result.size());
+  EXPECT_FALSE(result.ok());
+  EXPECT_EQ(0u, vector.size());
+}
+
+TEST(CopyIntoVectorTest, TooSmall_Truncates) {
+  pw::Vector<char, 2> vector{};
+  auto result = Copy("Hi!"sv, vector);
+  EXPECT_EQ(2u, result.size());
+  EXPECT_FALSE(result.ok());
+  EXPECT_EQ(2u, vector.size());
+  EXPECT_EQ("Hi"sv, std::string_view(vector.data(), vector.size()));
+}
+
+TEST(CopyIntoVectorTest, ExactFit) {
+  pw::Vector<char, 3> vector{};
+  auto result = Copy("Hi!", vector);
+  EXPECT_EQ(3u, result.size());
+  EXPECT_TRUE(result.ok());
+  EXPECT_EQ("Hi!"sv, std::string_view(vector.data(), vector.size()));
+}
+
+TEST(CopyIntoVectorTest, NullTerminatorsInString) {
+  pw::Vector<char, 32> vector{};
+  ASSERT_EQ(4u, Copy("\0!\0\0"sv, vector).size());
+  EXPECT_EQ("\0!\0\0"sv, std::string_view(vector.data(), vector.size()));
+}
+
+class TestWithBuffer : public ::testing::Test {
+ protected:
+  static constexpr char kStartingString[] = "!@#$%^&*()!@#$%^&*()";
+
+  TestWithBuffer() { std::memcpy(buffer_, kStartingString, sizeof(buffer_)); }
+
+  char buffer_[sizeof(kStartingString)];
+};
+
+class CopyFromVectorTest : public TestWithBuffer {};
+
+TEST_F(CopyFromVectorTest, EmptyVector_WritesNullTerminator) {
+  const pw::Vector<char, 32> vector{};
+  EXPECT_EQ(0u, Copy(vector, buffer_).size());
+  EXPECT_EQ('\0', buffer_[0]);
+}
+
+TEST_F(CopyFromVectorTest, EmptyBuffer_WritesNothing) {
+  const pw::Vector<char, 32> vector{'H', 'e', 'l', 'l', 'o'};
+  auto result = Copy(vector, std::span(buffer_, 0));
+  EXPECT_EQ(0u, result.size());
+  EXPECT_FALSE(result.ok());
+  EXPECT_STREQ(kStartingString, buffer_);
+}
+
+TEST_F(CopyFromVectorTest, TooSmall_Truncates) {
+  const pw::Vector<char, 32> vector{'H', 'i', '!'};
+  auto result = Copy(vector, std::span(buffer_, 3));
+  EXPECT_EQ(2u, result.size());
+  EXPECT_FALSE(result.ok());
+  EXPECT_STREQ("Hi", buffer_);
+}
+
+TEST_F(CopyFromVectorTest, ExactFit) {
+  const pw::Vector<char, 32> vector{'H', 'i', '!'};
+  auto result = Copy(vector, std::span(buffer_, 4));
+  EXPECT_EQ(3u, result.size());
+  EXPECT_TRUE(result.ok());
+  EXPECT_STREQ("Hi!", buffer_);
+}
+
+TEST_F(CopyFromVectorTest, NullTerminatorsInString) {
+  const pw::Vector<char, 32> vector{'\0', '!', '\0', '\0'};
+  ASSERT_EQ(4u, Copy(vector, std::span(buffer_, 5)).size());
+  EXPECT_EQ("\0!\0\0"sv, std::string_view(buffer_, 4));
+}
+
+}  // namespace
+}  // namespace pw::string