blob: 425fbfb630707907240f805681aa9a2b530b186e [file] [log] [blame]
// Copyright 2019 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 <algorithm>
#include <cstdarg>
#include <cstddef>
#include <cstring>
#include <span>
#include <string_view>
#include <type_traits>
#include <utility>
#include "pw_preprocessor/compiler.h"
#include "pw_status/status.h"
#include "pw_status/status_with_size.h"
#include "pw_string/to_string.h"
namespace pw {
// StringBuilder facilitates building formatted strings in a fixed-size buffer.
// StringBuilders are always null terminated (unless they are constructed with
// an empty buffer) and never overflow. Status is tracked for each operation and
// an overall status is maintained, which reflects the most recent error.
//
// A StringBuilder does not own the buffer it writes to. It can be used to write
// strings to any buffer. The StringBuffer template class, defined below,
// allocates a buffer alongside a StringBuilder.
//
// StringBuilder supports C++-style << output, similar to std::ostringstream. It
// also supports std::string-like append functions and printf-style output.
//
// StringBuilder uses the ToString function to support arbitrary types. Defining
// a ToString template specialization overload in the pw namespace allows
// writing that type to a StringBuilder with <<.
//
// For example, the following ToString overload allows writing MyStatus objects
// to StringBuilders:
//
// namespace pw {
//
// template <>
// StatusWithSize ToString<MyStatus>(MyStatus value, std::span<char> buffer) {
// return CopyString(MyStatusString(value), buffer);
// }
//
// } // namespace pw
//
// For complex types, it may be easier to override StringBuilder's << operator,
// similar to the standard library's std::ostream. For example:
//
// namespace pw {
//
// StringBuilder& operator<<(StringBuilder& sb, const MyType& value) {
// return sb << "MyType(" << value.foo << ", " << value.bar << ')';
// }
//
// } // namespace pw
//
// Alternately, complex types may use a StringBuilder in their ToString, but it
// is likely to be simpler to override StringBuilder's operator<<.
//
// StringBuilder is safe, flexible, and results in much smaller code size than
// using std::ostringstream. However, applications sensitive to code size should
// use StringBuilder with care.
//
// The fixed code size cost of StringBuilder is significant, though smaller than
// std::snprintf. Using StringBuilder's << and append methods exclusively in
// place of snprintf reduces code size, but snprintf may be difficult to avoid.
//
// The incremental code size cost of StringBuilder is comparable to snprintf if
// errors are handled. Each argument to StringBuilder's << expands to a function
// call, but one or two StringBuilder appends may have a smaller code size
// impact than a single snprintf call. See the size report for further analysis.
class StringBuilder {
public:
// Creates an empty StringBuilder.
constexpr StringBuilder(std::span<char> buffer) : buffer_(buffer), size_(0) {
NullTerminate();
}
StringBuilder(std::span<std::byte> buffer)
: StringBuilder(
{reinterpret_cast<char*>(buffer.data()), buffer.size_bytes()}) {}
// Disallow copy/assign to avoid confusion about where the string is actually
// stored. StringBuffers may be copied into one another.
StringBuilder(const StringBuilder&) = delete;
StringBuilder& operator=(const StringBuilder&) = delete;
// Returns the contents of the string buffer. Always null-terminated.
const char* data() const { return buffer_.data(); }
const char* c_str() const { return data(); }
// Returns a std::string_view of the contents of this StringBuilder. The
// std::string_view is invalidated if the StringBuilder contents change.
std::string_view view() const { return std::string_view(data(), size()); }
// Allow implicit conversions to std::string_view so StringBuilders can be
// passed into functions that take a std::string_view.
operator std::string_view() const { return view(); }
// Returns a std::span<const std::byte> representation of this StringBuffer.
std::span<const std::byte> as_bytes() const {
return std::span(reinterpret_cast<const std::byte*>(buffer_.data()), size_);
}
// Returns the StringBuilder's status, which reflects the most recent error
// that occurred while updating the string. After an update fails, the status
// remains non-OK until it is cleared with clear() or clear_status(). Returns:
//
// OK if no errors have occurred
// RESOURCE_EXHAUSTED if output to the StringBuilder was truncated
// INVALID_ARGUMENT if printf-style formatting failed
// OUT_OF_RANGE if an operation outside the buffer was attempted
//
Status status() const { return status_; }
// Returns status() and size() as a StatusWithSize.
StatusWithSize status_with_size() const {
return StatusWithSize(status_, size_);
}
// The status from the last operation. May be OK while status() is not OK.
Status last_status() const { return last_status_; }
// True if status() is Status::Ok().
bool ok() const { return status_.ok(); }
// True if the string is empty.
bool empty() const { return size() == 0u; }
// Returns the current length of the string, excluding the null terminator.
size_t size() const { return size_; }
// Returns the maximum length of the string, excluding the null terminator.
size_t max_size() const { return buffer_.empty() ? 0u : buffer_.size() - 1; }
// Clears the string and resets its error state.
void clear();
// Sets the statuses to Status::Ok();
void clear_status() {
status_ = Status::Ok();
last_status_ = Status::Ok();
}
// Appends a single character. Stets the status to RESOURCE_EXHAUSTED if the
// character cannot be added because the buffer is full.
void push_back(char ch) { append(1, ch); }
// Removes the last character. Sets the status to OUT_OF_RANGE if the buffer
// is empty (in which case the unsigned overflow is intentional).
void pop_back() PW_NO_SANITIZE("unsigned-integer-overflow") {
resize(size() - 1);
}
// Appends the provided character count times.
StringBuilder& append(size_t count, char ch);
// Appends count characters from str to the end of the StringBuilder. If count
// exceeds the remaining space in the StringBuffer, max_size() - size()
// characters are appended and the status is set to RESOURCE_EXHAUSTED.
//
// str is not considered null-terminated and may contain null characters.
StringBuilder& append(const char* str, size_t count);
// Appends characters from the null-terminated string to the end of the
// StringBuilder. If the string's length exceeds the remaining space in the
// buffer, max_size() - size() characters are copied and the status is set to
// RESOURCE_EXHAUSTED.
//
// This function uses string::Length instead of std::strlen to avoid unbounded
// reads if the string is not null terminated.
StringBuilder& append(const char* str);
// Appends a std::string_view to the end of the StringBuilder.
StringBuilder& append(const std::string_view& str);
// Appends a substring from the std::string_view to the StringBuilder. Copies
// up to count characters starting from pos to the end of the StringBuilder.
// If pos > str.size(), sets the status to OUT_OF_RANGE.
StringBuilder& append(const std::string_view& str,
size_t pos,
size_t count = std::string_view::npos);
// Appends to the end of the StringBuilder using the << operator. This enables
// C++ stream-style formatted to StringBuilders.
template <typename T>
StringBuilder& operator<<(const T& value) {
// For std::string_view-compatible types, use the append function, which
// gives smaller code size.
if constexpr (std::is_convertible_v<T, std::string_view>) {
append(value);
} else {
HandleStatusWithSize(ToString(value, buffer_.subspan(size_)));
}
return *this;
}
// Provide a few additional operator<< overloads that reduce code size.
StringBuilder& operator<<(bool value) {
return append(value ? "true" : "false");
}
StringBuilder& operator<<(char value) {
push_back(value);
return *this;
}
StringBuilder& operator<<(std::nullptr_t) {
return append(string::kNullPointerString);
}
StringBuilder& operator<<(Status status) { return *this << status.str(); }
// Appends a printf-style string to the end of the StringBuilder. If the
// formatted string does not fit, the results are truncated and the status is
// set to RESOURCE_EXHAUSTED.
//
// Internally, calls string::Format, which calls std::vsnprintf.
PW_PRINTF_FORMAT(2, 3) StringBuilder& Format(const char* format, ...);
// Appends a vsnprintf-style string with va_list arguments to the end of the
// StringBuilder. If the formatted string does not fit, the results are
// truncated and the status is set to RESOURCE_EXHAUSTED.
//
// Internally, calls string::Format, which calls std::vsnprintf.
StringBuilder& FormatVaList(const char* format, va_list args);
// Sets the StringBuilder's size. This function only truncates; if
// new_size > size(), it sets status to OUT_OF_RANGE and does nothing.
void resize(size_t new_size);
protected:
// Functions to support StringBuffer copies.
constexpr StringBuilder(std::span<char> buffer, const StringBuilder& other)
: buffer_(buffer),
size_(other.size_),
status_(other.status_),
last_status_(other.last_status_) {}
void CopySizeAndStatus(const StringBuilder& other);
private:
size_t ResizeAndTerminate(size_t chars_to_append);
void HandleStatusWithSize(StatusWithSize written);
constexpr void NullTerminate() {
if (!buffer_.empty()) {
buffer_[size_] = '\0';
}
}
void SetErrorStatus(Status status);
const std::span<char> buffer_;
size_t size_;
Status status_;
Status last_status_;
};
// StringBuffers declare a buffer along with a StringBuilder. StringBuffer can
// be used as a statically allocated replacement for std::ostringstream or
// std::string. For example:
//
// StringBuffer<32> str;
// str << "The answer is " << number << "!"; // with number = 42
// str.c_str(); // null terminated C string "The answer is 42."
// str.view(); // std::string_view of "The answer is 42."
//
template <size_t kSizeBytes>
class StringBuffer : public StringBuilder {
public:
StringBuffer() : StringBuilder(buffer_) {}
// StringBuffers of the same size may be copied and assigned into one another.
StringBuffer(const StringBuffer& other) : StringBuilder(buffer_, other) {
CopyContents(other);
}
// A smaller StringBuffer may be copied or assigned into a larger one.
template <size_t kOtherSizeBytes>
StringBuffer(const StringBuffer<kOtherSizeBytes>& other)
: StringBuilder(buffer_, other) {
static_assert(StringBuffer<kOtherSizeBytes>::max_size() <= max_size(),
"A StringBuffer cannot be copied into a smaller buffer");
CopyContents(other);
}
template <size_t kOtherSizeBytes>
StringBuffer& operator=(const StringBuffer<kOtherSizeBytes>& other) {
assign<kOtherSizeBytes>(other);
return *this;
}
StringBuffer& operator=(const StringBuffer& other) {
assign<kSizeBytes>(other);
return *this;
}
template <size_t kOtherSizeBytes>
StringBuffer& assign(const StringBuffer<kOtherSizeBytes>& other) {
static_assert(StringBuffer<kOtherSizeBytes>::max_size() <= max_size(),
"A StringBuffer cannot be copied into a smaller buffer");
CopySizeAndStatus(other);
CopyContents(other);
return *this;
}
// Returns the maximum length of the string, excluding the null terminator.
static constexpr size_t max_size() { return kSizeBytes - 1; }
// Returns a StringBuffer<kSizeBytes>& instead of a generic StringBuilder& for
// append calls and stream-style operations.
template <typename... Args>
StringBuffer& append(Args&&... args) {
StringBuilder::append(std::forward<Args>(args)...);
return *this;
}
template <typename T>
StringBuffer& operator<<(T&& value) {
static_cast<StringBuilder&>(*this) << std::forward<T>(value);
return *this;
}
private:
template <size_t kOtherSize>
void CopyContents(const StringBuffer<kOtherSize>& other) {
std::memcpy(buffer_, other.data(), other.size() + 1); // include the \0
}
static_assert(kSizeBytes >= 1u, "StringBuffers must be at least 1 byte long");
char buffer_[kSizeBytes];
};
namespace string_internal {
// Internal code for determining the default size of StringBuffers created with
// MakeString.
//
// StringBuffers created with MakeString default to at least 24 bytes. This is
// large enough to fit the largest 64-bit integer (20 digits plus a \0), rounded
// up to the nearest multiple of 4.
inline constexpr size_t kDefaultMinimumStringBufferSize = 24;
// By default, MakeString uses a buffer size large enough to fit all string
// literal arguments. ArgLength uses this value as an estimate of the number of
// characters needed to represent a non-string argument.
inline constexpr size_t kDefaultArgumentSize = 4;
// Returns a string literal's length or kDefaultArgumentSize for non-strings.
template <typename T>
constexpr size_t ArgLength() {
using Arg = std::remove_reference_t<T>;
// If the argument is an array of const char, assume it is a string literal.
if constexpr (std::is_array_v<Arg>) {
using Element = std::remove_reference_t<decltype(std::declval<Arg>()[0])>;
if constexpr (std::is_same_v<Element, const char>) {
return std::extent_v<Arg> > 0u ? std::extent_v<Arg> - 1 : size_t(0);
}
}
return kDefaultArgumentSize;
}
// This function returns the default string buffer size used by MakeString.
template <typename... Args>
constexpr size_t DefaultStringBufferSize() {
return std::max((size_t(1) + ... + ArgLength<Args>()),
kDefaultMinimumStringBufferSize);
}
// Internal version of MakeString with const reference arguments instead of
// deduced types, which include the lengths of string literals. Having this
// function can reduce code size.
template <size_t kBufferSize, typename... Args>
auto InitializeStringBuffer(const Args&... args) {
return (StringBuffer<kBufferSize>() << ... << args);
}
} // namespace string_internal
// Makes a StringBuffer with a string version of a series of values. This is
// useful for creating and initializing a StringBuffer or for conveniently
// getting a null-terminated string. For example:
//
// LOG_INFO("The MAC address is %s", MakeString(mac_address).c_str());
//
// By default, the buffer size is 24 bytes, large enough to fit any 64-bit
// integer. If string literal arguments are provided, the default size will be
// large enough to fit them and a null terminator, plus 4 additional bytes for
// each argument. To use a fixed buffer size, set the kBufferSize template
// argument. For example:
//
// // Creates a default-size StringBuffer (10 + 10 + 4 + 1 + 1 = 26 bytes).
// auto sb = MakeString("1234567890", "1234567890", number, "!");
//
// // Creates a 32-byte StringBuffer.
// auto sb = MakeString<32>("1234567890", "1234567890", number, "!");
//
// Keep in mind that each argument to MakeString expands to a function call.
// MakeString may increase code size more than an equivalent pw::string::Format
// (or std::snprintf) call.
template <size_t kBufferSize = 0u, typename... Args>
auto MakeString(Args&&... args) {
constexpr size_t kSize =
kBufferSize == 0u ? string_internal::DefaultStringBufferSize<Args...>()
: kBufferSize;
return string_internal::InitializeStringBuffer<kSize>(args...);
}
} // namespace pw