pw_string: add pw::string::Copy methods

Adds pw::string::Copy helper methods as a safer alternative to
std::strncpy which unfortunately does not always null terminate.

In addition, the existing StringCopy methods are renamed to
StringOrNullCopy to denote that they support nullptr source strings
unlike pw::string::Copy.

Change-Id: I046c12da02721c5ad2f6601b9ac742d9dfa90c71
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/42931
Reviewed-by: Ewout van Bekkum <ewout@google.com>
Reviewed-by: Wyatt Hepler <hepler@google.com>
Reviewed-by: Keir Mierle <keir@google.com>
Pigweed-Auto-Submit: Ewout van Bekkum <ewout@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed.google.com.iam.gserviceaccount.com>
diff --git a/pw_string/BUILD b/pw_string/BUILD
index 0347936..572a16d 100644
--- a/pw_string/BUILD
+++ b/pw_string/BUILD
@@ -40,6 +40,7 @@
     deps = [
         "//pw_preprocessor",
         "//pw_span",
+        "//pw_assert",
         "//pw_status",
     ],
 )
diff --git a/pw_string/BUILD.gn b/pw_string/BUILD.gn
index 5aa0134..caba610 100644
--- a/pw_string/BUILD.gn
+++ b/pw_string/BUILD.gn
@@ -38,6 +38,7 @@
     "type_to_string.cc",
   ]
   public_deps = [
+    "$dir_pw_assert",
     "$dir_pw_preprocessor",
     "$dir_pw_status",
   ]
diff --git a/pw_string/CMakeLists.txt b/pw_string/CMakeLists.txt
index 92a14a4..8b66bc0 100644
--- a/pw_string/CMakeLists.txt
+++ b/pw_string/CMakeLists.txt
@@ -16,6 +16,7 @@
 
 pw_auto_add_simple_module(pw_string
   PUBLIC_DEPS
+    pw_assert
     pw_preprocessor
     pw_span
     pw_status
diff --git a/pw_string/docs.rst b/pw_string/docs.rst
index 467908b..07028df 100644
--- a/pw_string/docs.rst
+++ b/pw_string/docs.rst
@@ -34,6 +34,36 @@
 
 .. include:: format_size_report
 
+pw::string::Length
+==================
+The ``pw::string::Length`` function provides a safer alternative to
+``std::strlen`` in case a string is extremely long and/or potentially not
+null-terminated. It is a constexpr version of C11's ``strnlen_s``.
+
+.. cpp:function:: constexpr size_t Length(const char* str, size_t max_len)
+
+   Calculates the length of a null-terminated string up to the specified maximum
+   length. If str is nullptr, returns 0.
+
+pw::string::Copy
+================
+The ``pw::string::Copy`` functions provide a safer alternative to
+``std::strncpy`` as it always null-terminates whenever the destination
+buffer has a non-zero size.
+
+.. 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)
+
+   Copies the source string to the dest, truncating if the full string does not
+   fit. Always null terminates if dest.size() or num > 0.
+
+   Returns the number of characters written, excluding the null terminator. 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.
+
 pw::StringBuilder
 =================
 ``pw::StringBuilder`` facilitates building formatted strings in a fixed-size
@@ -74,7 +104,7 @@
 
   template <>
   StatusWithSize ToString<MyStatus>(MyStatus value, std::span<char> buffer) {
-    return CopyString(MyStatusString(value), buffer);
+    return Copy(MyStatusString(value), buffer);
   }
 
   }  // namespace pw
diff --git a/pw_string/public/pw_string/string_builder.h b/pw_string/public/pw_string/string_builder.h
index c85e6be..5828e6e 100644
--- a/pw_string/public/pw_string/string_builder.h
+++ b/pw_string/public/pw_string/string_builder.h
@@ -65,7 +65,7 @@
 //
 //   template <>
 //   StatusWithSize ToString<MyStatus>(MyStatus value, std::span<char> buffer) {
-//     return CopyString(MyStatusString(value), buffer);
+//     return Copy(MyStatusString(value), buffer);
 //   }
 //
 //   }  // namespace pw
diff --git a/pw_string/public/pw_string/to_string.h b/pw_string/public/pw_string/to_string.h
index 463005f..8d9e64d 100644
--- a/pw_string/public/pw_string/to_string.h
+++ b/pw_string/public/pw_string/to_string.h
@@ -68,7 +68,7 @@
   if constexpr (std::is_same_v<std::remove_cv_t<T>, bool>) {
     return string::BoolToString(value, buffer);
   } else if constexpr (std::is_same_v<std::remove_cv_t<T>, char>) {
-    return string::CopyString(std::string_view(&value, 1), buffer);
+    return string::Copy(std::string_view(&value, 1), buffer);
   } else if constexpr (std::is_integral_v<T>) {
     return string::IntToString(value, buffer);
   } else if constexpr (std::is_enum_v<T>) {
@@ -76,7 +76,7 @@
   } else if constexpr (std::is_floating_point_v<T>) {
     return string::FloatAsIntToString(value, buffer);
   } else if constexpr (std::is_convertible_v<T, std::string_view>) {
-    return string::CopyString(value, buffer);
+    return string::CopyStringOrNull(value, buffer);
   } else if constexpr (std::is_pointer_v<std::remove_cv_t<T>> ||
                        std::is_null_pointer_v<T>) {
     return string::PointerToString(value, buffer);
@@ -89,7 +89,7 @@
 // ToString overloads for Pigweed types. To override ToString for a custom type,
 // specialize the ToString template function.
 inline StatusWithSize ToString(Status status, std::span<char> buffer) {
-  return string::CopyString(status.str(), buffer);
+  return string::Copy(status.str(), buffer);
 }
 
 inline StatusWithSize ToString(pw_Status status, std::span<char> buffer) {
diff --git a/pw_string/public/pw_string/type_to_string.h b/pw_string/public/pw_string/type_to_string.h
index 2841a60..9c32120 100644
--- a/pw_string/public/pw_string/type_to_string.h
+++ b/pw_string/public/pw_string/type_to_string.h
@@ -23,6 +23,7 @@
 #include <type_traits>
 
 #include "pw_status/status_with_size.h"
+#include "pw_string/util.h"
 
 namespace pw::string {
 
@@ -107,36 +108,46 @@
 // CopyEntireString.
 StatusWithSize PointerToString(const void* pointer, std::span<char> buffer);
 
+// Specialized form of pw::string::Copy which supports nullptr values.
+//
 // Copies the string to the buffer, truncating if the full string does not fit.
 // Always null terminates if buffer.size() > 0.
 //
+// If value is a nullptr, then "(null)" is used as a fallback.
+//
 // Returns the number of characters written, excluding the null terminator. If
 // the string is truncated, the status is RESOURCE_EXHAUSTED.
-StatusWithSize CopyString(const std::string_view& value,
-                          std::span<char> buffer);
-
-inline StatusWithSize CopyString(const char* value, std::span<char> buffer) {
+inline StatusWithSize CopyStringOrNull(const std::string_view& value,
+                                       std::span<char> buffer) {
+  return Copy(value, buffer);
+}
+inline StatusWithSize CopyStringOrNull(const char* value,
+                                       std::span<char> buffer) {
   if (value == nullptr) {
     return PointerToString(value, buffer);
   }
-  return CopyString(std::string_view(value), buffer);
+  return Copy(value, buffer);
 }
 
 // Copies the string to the buffer, if the entire string fits. Always null
 // terminates if buffer.size() > 0.
 //
+// If value is a nullptr, then "(null)" is used as a fallback.
+//
 // Returns the number of characters written, excluding the null terminator. If
 // the full string does not fit, only a null terminator is written and the
 // status is RESOURCE_EXHAUSTED.
-StatusWithSize CopyEntireString(const std::string_view& value,
-                                std::span<char> buffer);
+StatusWithSize CopyEntireStringOrNull(const std::string_view& value,
+                                      std::span<char> buffer);
 
-inline StatusWithSize CopyEntireString(const char* value,
-                                       std::span<char> buffer) {
+// Same as the string_view form of CopyEntireString, except that if value is a
+// nullptr, then "(null)" is used as a fallback.
+inline StatusWithSize CopyEntireStringOrNull(const char* value,
+                                             std::span<char> buffer) {
   if (value == nullptr) {
     return PointerToString(value, buffer);
   }
-  return CopyEntireString(std::string_view(value), buffer);
+  return CopyEntireStringOrNull(std::string_view(value), buffer);
 }
 
 // This function is a fallback that is called if by ToString if no overload
diff --git a/pw_string/public/pw_string/util.h b/pw_string/public/pw_string/util.h
index 37c6c0b..1909401 100644
--- a/pw_string/public/pw_string/util.h
+++ b/pw_string/public/pw_string/util.h
@@ -1,4 +1,4 @@
-// Copyright 2019 The Pigweed Authors
+// 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
@@ -14,6 +14,12 @@
 #pragma once
 
 #include <cstddef>
+#include <span>
+#include <string_view>
+
+#include "pw_assert/assert.h"
+#include "pw_status/status.h"
+#include "pw_status/status_with_size.h"
 
 namespace pw {
 namespace string {
@@ -36,5 +42,36 @@
   return length;
 }
 
+// Copies the source string to the dest, truncating if the full string does not
+// fit. Always null terminates if dest.size() or num > 0.
+//
+// Returns the number of characters written, excluding the null terminator. 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.
+constexpr StatusWithSize Copy(const std::string_view& source,
+                              std::span<char> dest) {
+  if (dest.empty()) {
+    return StatusWithSize::ResourceExhausted();
+  }
+
+  const size_t copied = source.copy(dest.data(), dest.size() - 1);
+  dest[copied] = '\0';
+
+  return StatusWithSize(
+      copied == source.size() ? OkStatus() : Status::ResourceExhausted(),
+      copied);
+}
+
+constexpr StatusWithSize Copy(const char* source, std::span<char> dest) {
+  PW_DASSERT(source != nullptr);
+  return Copy(std::string_view(source, Length(source, dest.size())), dest);
+}
+
+constexpr StatusWithSize Copy(const char* source, char* dest, size_t num) {
+  return Copy(source, std::span<char>(dest, num));
+}
+
 }  // namespace string
 }  // namespace pw
diff --git a/pw_string/type_to_string.cc b/pw_string/type_to_string.cc
index 4872504..6d2769e 100644
--- a/pw_string/type_to_string.cc
+++ b/pw_string/type_to_string.cc
@@ -170,32 +170,18 @@
 }
 
 StatusWithSize BoolToString(bool value, std::span<char> buffer) {
-  return CopyEntireString(value ? "true" : "false", buffer);
+  return CopyEntireStringOrNull(value ? "true" : "false", buffer);
 }
 
 StatusWithSize PointerToString(const void* pointer, std::span<char> buffer) {
   if (pointer == nullptr) {
-    return CopyEntireString(kNullPointerString, buffer);
+    return CopyEntireStringOrNull(kNullPointerString, buffer);
   }
   return IntToHexString(reinterpret_cast<uintptr_t>(pointer), buffer);
 }
 
-StatusWithSize CopyString(const std::string_view& value,
-                          std::span<char> buffer) {
-  if (buffer.empty()) {
-    return StatusWithSize::ResourceExhausted();
-  }
-
-  const size_t copied = value.copy(buffer.data(), buffer.size() - 1);
-  buffer[copied] = '\0';
-
-  return StatusWithSize(
-      copied == value.size() ? OkStatus() : Status::ResourceExhausted(),
-      copied);
-}
-
-StatusWithSize CopyEntireString(const std::string_view& value,
-                                std::span<char> buffer) {
+StatusWithSize CopyEntireStringOrNull(const std::string_view& value,
+                                      std::span<char> buffer) {
   if (value.size() >= buffer.size()) {
     return HandleExhaustedBuffer(buffer);
   }
diff --git a/pw_string/type_to_string_test.cc b/pw_string/type_to_string_test.cc
index 4deb657..ffadc27d 100644
--- a/pw_string/type_to_string_test.cc
+++ b/pw_string/type_to_string_test.cc
@@ -407,71 +407,84 @@
   EXPECT_STREQ("", buffer_);
 }
 
-class CopyStringTest : public TestWithBuffer {};
+class CopyStringOrNullTest : public TestWithBuffer {};
 
 using namespace std::literals::string_view_literals;
 
-TEST_F(CopyStringTest, EmptyStringView_WritesNullTerminator) {
-  EXPECT_EQ(0u, CopyString("", buffer_).size());
+TEST_F(CopyStringOrNullTest, NullSource_WritesNullPointerString) {
+  EXPECT_EQ(kNullPointerString.size(),
+            CopyStringOrNull(nullptr, buffer_).size());
+  EXPECT_EQ(kNullPointerString, buffer_);
+}
+
+TEST_F(CopyStringOrNullTest, EmptyStringView_WritesNullTerminator) {
+  EXPECT_EQ(0u, CopyStringOrNull("", buffer_).size());
   EXPECT_EQ('\0', buffer_[0]);
 }
 
-TEST_F(CopyStringTest, EmptyBuffer_WritesNothing) {
-  auto result = CopyString("Hello", std::span(buffer_, 0));
+TEST_F(CopyStringOrNullTest, EmptyBuffer_WritesNothing) {
+  auto result = CopyStringOrNull("Hello", std::span(buffer_, 0));
   EXPECT_EQ(0u, result.size());
   EXPECT_FALSE(result.ok());
   EXPECT_STREQ(kStartingString, buffer_);
 }
 
-TEST_F(CopyStringTest, TooSmall_Truncates) {
-  auto result = CopyString("Hi!", std::span(buffer_, 3));
+TEST_F(CopyStringOrNullTest, TooSmall_Truncates) {
+  auto result = CopyStringOrNull("Hi!", std::span(buffer_, 3));
   EXPECT_EQ(2u, result.size());
   EXPECT_FALSE(result.ok());
   EXPECT_STREQ("Hi", buffer_);
 }
 
-TEST_F(CopyStringTest, ExactFit) {
-  auto result = CopyString("Hi!", std::span(buffer_, 4));
+TEST_F(CopyStringOrNullTest, ExactFit) {
+  auto result = CopyStringOrNull("Hi!", std::span(buffer_, 4));
   EXPECT_EQ(3u, result.size());
   EXPECT_TRUE(result.ok());
   EXPECT_STREQ("Hi!", buffer_);
 }
 
-TEST_F(CopyStringTest, NullTerminatorsInString) {
-  ASSERT_EQ(4u, CopyString("\0!\0\0"sv, std::span(buffer_, 5)).size());
+TEST_F(CopyStringOrNullTest, NullTerminatorsInString) {
+  ASSERT_EQ(4u, CopyStringOrNull("\0!\0\0"sv, std::span(buffer_, 5)).size());
   EXPECT_EQ("\0!\0\0"sv, std::string_view(buffer_, 4));
 }
 
-class CopyEntireStringTest : public TestWithBuffer {};
+class CopyEntireStringOrNullTest : public TestWithBuffer {};
 
-TEST_F(CopyEntireStringTest, EmptyStringView_WritesNullTerminator) {
-  EXPECT_EQ(0u, CopyEntireString("", buffer_).size());
+TEST_F(CopyEntireStringOrNullTest, NullSource_WritesNullPointerString) {
+  EXPECT_EQ(kNullPointerString.size(),
+            CopyEntireStringOrNull(nullptr, buffer_).size());
+  EXPECT_EQ(kNullPointerString, buffer_);
+}
+
+TEST_F(CopyEntireStringOrNullTest, EmptyStringView_WritesNullTerminator) {
+  EXPECT_EQ(0u, CopyEntireStringOrNull("", buffer_).size());
   EXPECT_EQ('\0', buffer_[0]);
 }
 
-TEST_F(CopyEntireStringTest, EmptyBuffer_WritesNothing) {
-  auto result = CopyEntireString("Hello", std::span(buffer_, 0));
+TEST_F(CopyEntireStringOrNullTest, EmptyBuffer_WritesNothing) {
+  auto result = CopyEntireStringOrNull("Hello", std::span(buffer_, 0));
   EXPECT_EQ(0u, result.size());
   EXPECT_FALSE(result.ok());
   EXPECT_STREQ(kStartingString, buffer_);
 }
 
-TEST_F(CopyEntireStringTest, TooSmall_WritesNothing) {
-  auto result = CopyEntireString("Hi!", std::span(buffer_, 3));
+TEST_F(CopyEntireStringOrNullTest, TooSmall_WritesNothing) {
+  auto result = CopyEntireStringOrNull("Hi!", std::span(buffer_, 3));
   EXPECT_EQ(0u, result.size());
   EXPECT_FALSE(result.ok());
   EXPECT_STREQ("", buffer_);
 }
 
-TEST_F(CopyEntireStringTest, ExactFit) {
-  auto result = CopyEntireString("Hi!", std::span(buffer_, 4));
+TEST_F(CopyEntireStringOrNullTest, ExactFit) {
+  auto result = CopyEntireStringOrNull("Hi!", std::span(buffer_, 4));
   EXPECT_EQ(3u, result.size());
   EXPECT_TRUE(result.ok());
   EXPECT_STREQ("Hi!", buffer_);
 }
 
-TEST_F(CopyEntireStringTest, NullTerminatorsInString) {
-  ASSERT_EQ(4u, CopyEntireString("\0!\0\0"sv, std::span(buffer_, 5)).size());
+TEST_F(CopyEntireStringOrNullTest, NullTerminatorsInString) {
+  ASSERT_EQ(4u,
+            CopyEntireStringOrNull("\0!\0\0"sv, std::span(buffer_, 5)).size());
   EXPECT_EQ("\0!\0\0"sv, std::string_view(buffer_, 4));
 }
 
diff --git a/pw_string/util_test.cc b/pw_string/util_test.cc
index 3815d3e..86c8562 100644
--- a/pw_string/util_test.cc
+++ b/pw_string/util_test.cc
@@ -1,4 +1,4 @@
-// Copyright 2019 The Pigweed Authors
+// 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
@@ -37,5 +37,49 @@
 
 TEST(Length, LengthEqualsMax) { EXPECT_EQ(5u, Length("12345", 5)); }
 
+class TestWithBuffer : public ::testing::Test {
+ protected:
+  static constexpr char kStartingString[] = "!@#$%^&*()!@#$%^&*()";
+
+  TestWithBuffer() { std::memcpy(buffer_, kStartingString, sizeof(buffer_)); }
+
+  char buffer_[sizeof(kStartingString)];
+};
+
+class CopyTest : public TestWithBuffer {};
+
+using namespace std::literals::string_view_literals;
+
+TEST_F(CopyTest, EmptyStringView_WritesNullTerminator) {
+  EXPECT_EQ(0u, Copy("", buffer_).size());
+  EXPECT_EQ('\0', buffer_[0]);
+}
+
+TEST_F(CopyTest, EmptyBuffer_WritesNothing) {
+  auto result = Copy("Hello", std::span(buffer_, 0));
+  EXPECT_EQ(0u, result.size());
+  EXPECT_FALSE(result.ok());
+  EXPECT_STREQ(kStartingString, buffer_);
+}
+
+TEST_F(CopyTest, TooSmall_Truncates) {
+  auto result = Copy("Hi!", std::span(buffer_, 3));
+  EXPECT_EQ(2u, result.size());
+  EXPECT_FALSE(result.ok());
+  EXPECT_STREQ("Hi", buffer_);
+}
+
+TEST_F(CopyTest, ExactFit) {
+  auto result = Copy("Hi!", std::span(buffer_, 4));
+  EXPECT_EQ(3u, result.size());
+  EXPECT_TRUE(result.ok());
+  EXPECT_STREQ("Hi!", buffer_);
+}
+
+TEST_F(CopyTest, NullTerminatorsInString) {
+  ASSERT_EQ(4u, Copy("\0!\0\0"sv, std::span(buffer_, 5)).size());
+  EXPECT_EQ("\0!\0\0"sv, std::string_view(buffer_, 4));
+}
+
 }  // namespace
 }  // namespace pw::string