diff --git a/esphome/core/string_ref.h b/esphome/core/string_ref.h index 34ba2474b2a..33459f48afa 100644 --- a/esphome/core/string_ref.h +++ b/esphome/core/string_ref.h @@ -76,6 +76,13 @@ class StringRef { constexpr bool empty() const { return len_ == 0; } constexpr const_reference operator[](size_type pos) const { return *(base_ + pos); } + /// True if the view begins with the given prefix (std::string::starts_with-like) + bool starts_with(const StringRef &prefix) const { + return len_ >= prefix.len_ && std::memcmp(base_, prefix.base_, prefix.len_) == 0; + } + bool starts_with(const char *prefix) const { return this->starts_with(StringRef(prefix)); } + bool starts_with(const std::string &prefix) const { return this->starts_with(StringRef(prefix)); } + /// Copy characters to destination buffer (std::string::copy-like, but returns 0 instead of throwing on out-of-range) size_type copy(char *dest, size_type count, size_type pos = 0) const { if (pos >= len_) diff --git a/tests/components/core/test_string_ref.cpp b/tests/components/core/test_string_ref.cpp new file mode 100644 index 00000000000..bcbd0aa0d46 --- /dev/null +++ b/tests/components/core/test_string_ref.cpp @@ -0,0 +1,62 @@ +#include + +#include "esphome/core/string_ref.h" + +namespace esphome::core::testing { + +TEST(StringRefStartsWith, ProperPrefixMatches) { + StringRef ref("FR:R20:12345", 12); + EXPECT_TRUE(ref.starts_with("FR:")); +} + +TEST(StringRefStartsWith, WholeStringIsAPrefixOfItself) { + StringRef ref("TP96", 4); + EXPECT_TRUE(ref.starts_with("TP96")); +} + +TEST(StringRefStartsWith, PrefixLongerThanViewFails) { + StringRef ref("TP", 2); + EXPECT_FALSE(ref.starts_with("TP96")); +} + +TEST(StringRefStartsWith, DifferentContentFails) { + StringRef ref("TP96", 4); + EXPECT_FALSE(ref.starts_with("FR:")); +} + +TEST(StringRefStartsWith, EmptyPrefixAlwaysMatches) { + StringRef ref("abc", 3); + EXPECT_TRUE(ref.starts_with("")); + StringRef empty; + EXPECT_TRUE(empty.starts_with("")); +} + +TEST(StringRefStartsWith, EmptyViewOnlyMatchesEmptyPrefix) { + StringRef empty; + EXPECT_FALSE(empty.starts_with("a")); +} + +TEST(StringRefStartsWith, WorksOnANonTerminatedBuffer) { + // The reason the helper exists: a bounded view over a buffer with no + // terminator anywhere near the viewed bytes. + const char raw[] = {'R', 'a', 'd', 'o', 'n', 'X'}; + StringRef ref(raw, 5); + EXPECT_TRUE(ref.starts_with("Radon")); + EXPECT_FALSE(ref.starts_with("RadonEye")); + EXPECT_FALSE(ref.starts_with("adon")); +} + +TEST(StringRefStartsWith, StdStringOverload) { + StringRef ref("TP96", 4); + EXPECT_TRUE(ref.starts_with(std::string("TP"))); + EXPECT_FALSE(ref.starts_with(std::string("96"))); +} + +TEST(StringRefStartsWith, RefOverloadComparesOnlyTheViewedLength) { + // The prefix is a bounded view: bytes past its length must not be compared. + StringRef ref("FR:123", 6); + StringRef prefix("FR:xyz", 3); + EXPECT_TRUE(ref.starts_with(prefix)); +} + +} // namespace esphome::core::testing