Add Format option for buffer writers and string builders (#27572)

* Add Format option for buffer writers and string builders

* Update comments

* Updated to only have printf inside stringbuilder and NOT bufferwriter

* Restyle

* Add missing file

* remove cpp file comments

* Fix cast to make clang happy

* Never pass null pointer in vsnprintf, since our size available is never 0

* Restyle

* Update src/lib/support/StringBuilder.cpp

Co-authored-by: Damian Królik <66667989+Damian-Nordic@users.noreply.github.com>

---------

Co-authored-by: Andrei Litvin <andreilitvin@google.com>
Co-authored-by: Damian Królik <66667989+Damian-Nordic@users.noreply.github.com>
diff --git a/src/lib/support/BUILD.gn b/src/lib/support/BUILD.gn
index 618adc4..68b39eb 100644
--- a/src/lib/support/BUILD.gn
+++ b/src/lib/support/BUILD.gn
@@ -132,6 +132,8 @@
     "SetupDiscriminator.h",
     "SortUtils.h",
     "StateMachine.h",
+    "StringBuilder.cpp",
+    "StringBuilder.h",
     "StringSplitter.h",
     "ThreadOperationalDataset.cpp",
     "ThreadOperationalDataset.h",
diff --git a/src/lib/support/BufferWriter.cpp b/src/lib/support/BufferWriter.cpp
index 1d7da85..e939470 100644
--- a/src/lib/support/BufferWriter.cpp
+++ b/src/lib/support/BufferWriter.cpp
@@ -23,11 +23,7 @@
 BufferWriter & BufferWriter::Put(const char * s)
 {
     static_assert(CHAR_BIT == 8, "We're assuming char and uint8_t are the same size");
-    while (*s != 0)
-    {
-        Put(static_cast<uint8_t>(*s++));
-    }
-    return *this;
+    return Put(s, strlen(s));
 }
 
 BufferWriter & BufferWriter::Put(const void * buf, size_t len)
diff --git a/src/lib/support/StringBuilder.cpp b/src/lib/support/StringBuilder.cpp
new file mode 100644
index 0000000..febabc0
--- /dev/null
+++ b/src/lib/support/StringBuilder.cpp
@@ -0,0 +1,67 @@
+/*
+ *
+ *    Copyright (c) 2023 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    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
+ *
+ *        http://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 "StringBuilder.h"
+
+namespace chip {
+
+StringBuilderBase & StringBuilderBase::AddFormat(const char * format, ...)
+{
+    va_list args;
+    va_start(args, format);
+
+    char * output = nullptr;
+    if (mWriter.Available() > 0)
+    {
+        output = reinterpret_cast<char *>(mWriter.Buffer() + mWriter.Needed());
+    }
+    else
+    {
+        output = reinterpret_cast<char *>(mWriter.Buffer() + mWriter.Size());
+    }
+
+    // the + 1 size here because StringBuilder reserves one byte for final null terminator
+    int needed = vsnprintf(output, mWriter.Available() + 1, format, args);
+
+    // on invalid formats, printf-style methods return negative numbers
+    if (needed > 0)
+    {
+        mWriter.Skip(static_cast<size_t>(needed));
+    }
+
+    va_end(args);
+    NullTerminate();
+    return *this;
+}
+
+StringBuilderBase & StringBuilderBase::AddMarkerIfOverflow()
+{
+    if (mWriter.Fit())
+    {
+        return *this;
+    }
+
+    for (unsigned i = 0; i < 3; i++)
+    {
+        if (mWriter.Size() >= i + 1)
+        {
+            mWriter.Buffer()[mWriter.Size() - i - 1] = '.';
+        }
+    }
+    return *this;
+}
+} // namespace chip
diff --git a/src/lib/support/StringBuilder.h b/src/lib/support/StringBuilder.h
index 85452c2..9de1538 100644
--- a/src/lib/support/StringBuilder.h
+++ b/src/lib/support/StringBuilder.h
@@ -56,6 +56,13 @@
     /// Was nothing written yet?
     bool Empty() const { return mWriter.Needed() == 0; }
 
+    /// Write a formatted string to the stringbuilder
+    StringBuilderBase & AddFormat(const char * format, ...) ENFORCE_FORMAT(2, 3);
+
+    /// For strings we often want to know when they were truncated. If the underlying writer did
+    /// not fit, this replaces the last 3 characters with "."
+    StringBuilderBase & AddMarkerIfOverflow();
+
     /// access the underlying value
     const char * c_str() const { return reinterpret_cast<const char *>(mWriter.Buffer()); }
 
diff --git a/src/lib/support/tests/TestStringBuilder.cpp b/src/lib/support/tests/TestStringBuilder.cpp
index 2886d6f..34d0430 100644
--- a/src/lib/support/tests/TestStringBuilder.cpp
+++ b/src/lib/support/tests/TestStringBuilder.cpp
@@ -78,11 +78,154 @@
     }
 }
 
+void TestFormat(nlTestSuite * inSuite, void * inContext)
+{
+    {
+        StringBuilder<100> builder;
+
+        builder.AddFormat("Test: %d Hello %s\n", 123, "world");
+
+        NL_TEST_ASSERT(inSuite, builder.Fit());
+        NL_TEST_ASSERT(inSuite, strcmp(builder.c_str(), "Test: 123 Hello world\n") == 0);
+    }
+
+    {
+        StringBuilder<100> builder;
+
+        builder.AddFormat("Align: %-5s", "abc");
+
+        NL_TEST_ASSERT(inSuite, builder.Fit());
+        NL_TEST_ASSERT(inSuite, strcmp(builder.c_str(), "Align: abc  ") == 0);
+    }
+
+    {
+        StringBuilder<100> builder;
+
+        builder.AddFormat("Multi: %d", 1234);
+        builder.AddFormat(", then 0x%04X", 0xab);
+
+        NL_TEST_ASSERT(inSuite, builder.Fit());
+        NL_TEST_ASSERT(inSuite, strcmp(builder.c_str(), "Multi: 1234, then 0x00AB") == 0);
+    }
+}
+
+void TestFormatOverflow(nlTestSuite * inSuite, void * inContext)
+{
+    {
+        StringBuilder<13> builder;
+
+        builder.AddFormat("Test: %d Hello %s\n", 123, "world");
+
+        NL_TEST_ASSERT(inSuite, !builder.Fit());
+        NL_TEST_ASSERT(inSuite, strcmp(builder.c_str(), "Test: 123 He") == 0);
+    }
+
+    {
+        StringBuilder<11> builder;
+
+        builder.AddFormat("%d %d %d %d %d", 1, 2, 3, 4, 1234);
+
+        NL_TEST_ASSERT(inSuite, !builder.Fit());
+        NL_TEST_ASSERT(inSuite, strcmp(builder.c_str(), "1 2 3 4 12") == 0);
+
+        builder.AddMarkerIfOverflow();
+        NL_TEST_ASSERT(inSuite, strcmp(builder.c_str(), "1 2 3 4...") == 0);
+    }
+
+    {
+        StringBuilder<11> builder;
+
+        builder.AddFormat("%d", 1234);
+        NL_TEST_ASSERT(inSuite, builder.Fit());
+        NL_TEST_ASSERT(inSuite, strcmp(builder.c_str(), "1234") == 0);
+
+        builder.AddFormat("%s", "abc");
+        NL_TEST_ASSERT(inSuite, builder.Fit());
+        NL_TEST_ASSERT(inSuite, strcmp(builder.c_str(), "1234abc") == 0);
+
+        builder.AddMarkerIfOverflow(); // no overflow
+        NL_TEST_ASSERT(inSuite, strcmp(builder.c_str(), "1234abc") == 0);
+
+        builder.AddFormat("%08x", 0x123456);
+        NL_TEST_ASSERT(inSuite, !builder.Fit());
+        NL_TEST_ASSERT(inSuite, strcmp(builder.c_str(), "1234abc001") == 0);
+
+        builder.AddMarkerIfOverflow();
+        NL_TEST_ASSERT(inSuite, strcmp(builder.c_str(), "1234abc...") == 0);
+    }
+}
+
+void TestOverflowMarker(nlTestSuite * inSuite, void * inContext)
+{
+    {
+        StringBuilder<1> builder; // useless builder, but ok
+
+        builder.Add("abc123");
+
+        NL_TEST_ASSERT(inSuite, !builder.Fit());
+        NL_TEST_ASSERT(inSuite, strcmp(builder.c_str(), "") == 0);
+
+        builder.AddMarkerIfOverflow();
+        NL_TEST_ASSERT(inSuite, strcmp(builder.c_str(), "") == 0);
+    }
+
+    {
+        StringBuilder<2> builder;
+
+        builder.Add("abc123");
+
+        NL_TEST_ASSERT(inSuite, !builder.Fit());
+        NL_TEST_ASSERT(inSuite, strcmp(builder.c_str(), "a") == 0);
+
+        builder.AddMarkerIfOverflow();
+        NL_TEST_ASSERT(inSuite, strcmp(builder.c_str(), ".") == 0);
+    }
+
+    {
+        StringBuilder<3> builder;
+
+        builder.Add("abc123");
+
+        NL_TEST_ASSERT(inSuite, !builder.Fit());
+        NL_TEST_ASSERT(inSuite, strcmp(builder.c_str(), "ab") == 0);
+
+        builder.AddMarkerIfOverflow();
+        NL_TEST_ASSERT(inSuite, strcmp(builder.c_str(), "..") == 0);
+    }
+
+    {
+        StringBuilder<4> builder;
+
+        builder.Add("abc123");
+
+        NL_TEST_ASSERT(inSuite, !builder.Fit());
+        NL_TEST_ASSERT(inSuite, strcmp(builder.c_str(), "abc") == 0);
+
+        builder.AddMarkerIfOverflow();
+        NL_TEST_ASSERT(inSuite, strcmp(builder.c_str(), "...") == 0);
+    }
+
+    {
+        StringBuilder<5> builder;
+
+        builder.Add("abc123");
+
+        NL_TEST_ASSERT(inSuite, !builder.Fit());
+        NL_TEST_ASSERT(inSuite, strcmp(builder.c_str(), "abc1") == 0);
+
+        builder.AddMarkerIfOverflow();
+        NL_TEST_ASSERT(inSuite, strcmp(builder.c_str(), "a...") == 0);
+    }
+}
+
 const nlTest sTests[] = {
-    NL_TEST_DEF("TestStringBuilder", TestStringBuilder), //
-    NL_TEST_DEF("TestIntegerAppend", TestIntegerAppend), //
-    NL_TEST_DEF("TestOverflow", TestOverflow),           //
-    NL_TEST_SENTINEL()                                   //
+    NL_TEST_DEF("TestStringBuilder", TestStringBuilder),   //
+    NL_TEST_DEF("TestIntegerAppend", TestIntegerAppend),   //
+    NL_TEST_DEF("TestOverflow", TestOverflow),             //
+    NL_TEST_DEF("TestFormat", TestFormat),                 //
+    NL_TEST_DEF("TestFormatOverflow", TestFormatOverflow), //
+    NL_TEST_DEF("TestOverflowMarker", TestOverflowMarker), //
+    NL_TEST_SENTINEL()                                     //
 };
 
 } // namespace