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..ffadc27 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