mirror of
https://github.com/esphome/esphome.git
synced 2026-08-23 06:36:23 +00:00
[core] Move JSON string escaping into helpers (#17879)
This commit is contained in:
@@ -1,23 +0,0 @@
|
||||
"""Test-manifest overrides for the captive_portal C++ unit tests.
|
||||
|
||||
``json_escape`` lives in a standalone, dependency-free header
|
||||
(``esphome/components/captive_portal/json_escape.h``). The rest of the
|
||||
captive_portal component and its auto-loaded dependencies (``web_server_base``,
|
||||
``ota.web_server``) do not build for the ``host`` platform that the C++ unit
|
||||
test harness targets. Strip those away and replace the real schema -- which is
|
||||
restricted to non-host platforms via ``cv.only_on`` and requires a
|
||||
``web_server_base`` instance via ``use_id`` -- with an empty one so the host
|
||||
test config validates. ``to_code`` stays suppressed (the default), so
|
||||
``USE_CAPTIVE_PORTAL`` is never defined and ``captive_portal.cpp`` compiles to an
|
||||
empty translation unit; only ``json_escape.h`` is exercised by the test.
|
||||
"""
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from tests.testing_helpers import ComponentManifestOverride
|
||||
|
||||
|
||||
def override_manifest(manifest: ComponentManifestOverride) -> None:
|
||||
manifest.auto_load = []
|
||||
manifest.dependencies = []
|
||||
manifest.config_schema = cv.Schema({})
|
||||
manifest.final_validate_schema = None
|
||||
+41
-14
@@ -2,9 +2,10 @@
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "esphome/components/captive_portal/json_escape.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
namespace esphome::captive_portal::testing {
|
||||
namespace esphome::testing {
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -17,30 +18,36 @@ std::string escape(const std::string &value) {
|
||||
return json_escape_into_buffer(buf, StringRef(value.c_str(), value.size()));
|
||||
}
|
||||
|
||||
// Same, but with the short control forms turned off.
|
||||
std::string escape_long(const std::string &value) {
|
||||
char buf[TEST_BUFFER_SIZE];
|
||||
return json_escape_into_buffer(buf, StringRef(value.c_str(), value.size()), false);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Plain ASCII with no special characters is passed through unchanged.
|
||||
TEST(CaptivePortalJsonEscape, PlainStringUnchanged) {
|
||||
TEST(JsonEscape, PlainStringUnchanged) {
|
||||
EXPECT_EQ(escape("MyNetwork"), "MyNetwork");
|
||||
EXPECT_EQ(escape(""), "");
|
||||
}
|
||||
|
||||
// A double quote is escaped so it does not terminate the surrounding JSON string.
|
||||
TEST(CaptivePortalJsonEscape, EscapesDoubleQuote) {
|
||||
TEST(JsonEscape, EscapesDoubleQuote) {
|
||||
EXPECT_EQ(escape("a\"b"), "a\\\"b");
|
||||
// A double quote followed by other characters stays inside the JSON string.
|
||||
EXPECT_EQ(escape("\">end"), "\\\">end");
|
||||
}
|
||||
|
||||
// A backslash is doubled so it does not start an escape sequence in the output.
|
||||
TEST(CaptivePortalJsonEscape, EscapesBackslash) {
|
||||
TEST(JsonEscape, EscapesBackslash) {
|
||||
EXPECT_EQ(escape("a\\b"), "a\\\\b");
|
||||
// A trailing backslash must not escape the closing quote of the JSON string.
|
||||
EXPECT_EQ(escape("net\\"), "net\\\\");
|
||||
}
|
||||
|
||||
// The control characters with short JSON forms use those forms.
|
||||
TEST(CaptivePortalJsonEscape, EscapesShortFormControls) {
|
||||
TEST(JsonEscape, EscapesShortFormControls) {
|
||||
EXPECT_EQ(escape("\n"), "\\n");
|
||||
EXPECT_EQ(escape("\r"), "\\r");
|
||||
EXPECT_EQ(escape("\t"), "\\t");
|
||||
@@ -49,7 +56,7 @@ TEST(CaptivePortalJsonEscape, EscapesShortFormControls) {
|
||||
}
|
||||
|
||||
// Other control characters (< 0x20) without a short form become \u00XX with lowercase hex.
|
||||
TEST(CaptivePortalJsonEscape, EscapesOtherControlsAsUnicode) {
|
||||
TEST(JsonEscape, EscapesOtherControlsAsUnicode) {
|
||||
EXPECT_EQ(escape(std::string("\x00", 1)), "\\u0000");
|
||||
EXPECT_EQ(escape("\x01"), "\\u0001");
|
||||
EXPECT_EQ(escape("\x10"), "\\u0010");
|
||||
@@ -58,8 +65,28 @@ TEST(CaptivePortalJsonEscape, EscapesOtherControlsAsUnicode) {
|
||||
EXPECT_EQ(escape("\x7f"), "\x7f");
|
||||
}
|
||||
|
||||
// With the short forms turned off, every control character is written as \u00XX instead.
|
||||
TEST(JsonEscape, LongControlEscapes) {
|
||||
EXPECT_EQ(escape_long("\n"), "\\u000a");
|
||||
EXPECT_EQ(escape_long("\r"), "\\u000d");
|
||||
EXPECT_EQ(escape_long("\t"), "\\u0009");
|
||||
EXPECT_EQ(escape_long("\b"), "\\u0008");
|
||||
EXPECT_EQ(escape_long("\f"), "\\u000c");
|
||||
// Controls without a short form are unaffected by the flag.
|
||||
EXPECT_EQ(escape_long("\x01"), "\\u0001");
|
||||
}
|
||||
|
||||
// The flag only affects control characters. A quote or backslash is never written as \u00XX, because that form is
|
||||
// no shorter and both modes have always emitted the two character escape.
|
||||
TEST(JsonEscape, LongModeStillUsesTwoCharQuoteAndBackslash) {
|
||||
EXPECT_EQ(escape_long("a\"b"), "a\\\"b");
|
||||
EXPECT_EQ(escape_long("a\\b"), "a\\\\b");
|
||||
// Ordinary text is untouched in either mode.
|
||||
EXPECT_EQ(escape_long("MyDevice"), "MyDevice");
|
||||
}
|
||||
|
||||
// Bytes >= 0x20, including multi-byte UTF-8 sequences, are passed through verbatim.
|
||||
TEST(CaptivePortalJsonEscape, PassesThroughUtf8) {
|
||||
TEST(JsonEscape, PassesThroughUtf8) {
|
||||
// "café" in UTF-8 (é == 0xC3 0xA9).
|
||||
EXPECT_EQ(escape("caf\xc3\xa9"), "caf\xc3\xa9");
|
||||
// Emoji (📶, 4-byte UTF-8) survives unchanged.
|
||||
@@ -67,10 +94,10 @@ TEST(CaptivePortalJsonEscape, PassesThroughUtf8) {
|
||||
}
|
||||
|
||||
// A mix of special and normal characters is escaped in place without disturbing the rest.
|
||||
TEST(CaptivePortalJsonEscape, MixedContent) { EXPECT_EQ(escape("a\"b\\c\nd"), "a\\\"b\\\\c\\nd"); }
|
||||
TEST(JsonEscape, MixedContent) { EXPECT_EQ(escape("a\"b\\c\nd"), "a\\\"b\\\\c\\nd"); }
|
||||
|
||||
// A buffer sized at JSON_ESCAPE_MAX_EXPANSION bytes per input byte holds the worst case exactly.
|
||||
TEST(CaptivePortalJsonEscape, WorstCaseInputFitsExactly) {
|
||||
TEST(JsonEscape, WorstCaseInputFitsExactly) {
|
||||
constexpr size_t input_len = 8;
|
||||
char buf[input_len * JSON_ESCAPE_MAX_EXPANSION + 1];
|
||||
const std::string input(input_len, '\x01');
|
||||
@@ -82,7 +109,7 @@ TEST(CaptivePortalJsonEscape, WorstCaseInputFitsExactly) {
|
||||
|
||||
// An escape sequence that would not fit is dropped whole rather than written partially, and the result stays null
|
||||
// terminated.
|
||||
TEST(CaptivePortalJsonEscape, DropsEscapeThatWouldNotFit) {
|
||||
TEST(JsonEscape, DropsEscapeThatWouldNotFit) {
|
||||
// Room for one \u00XX sequence plus the null terminator, but two are requested.
|
||||
char buf[JSON_ESCAPE_MAX_EXPANSION + 1];
|
||||
const std::string input(2, '\x01');
|
||||
@@ -92,16 +119,16 @@ TEST(CaptivePortalJsonEscape, DropsEscapeThatWouldNotFit) {
|
||||
}
|
||||
|
||||
// Plain characters are truncated at the buffer size, leaving room for the null terminator.
|
||||
TEST(CaptivePortalJsonEscape, TruncatesPlainInput) {
|
||||
TEST(JsonEscape, TruncatesPlainInput) {
|
||||
char buf[5];
|
||||
const std::string input(20, 'a');
|
||||
EXPECT_STREQ(json_escape_into_buffer(buf, StringRef(input.c_str(), input.size())), "aaaa");
|
||||
}
|
||||
|
||||
// A zero length buffer cannot even hold a null terminator, so an empty string is returned instead of writing.
|
||||
TEST(CaptivePortalJsonEscape, EmptyBufferIsSafe) {
|
||||
TEST(JsonEscape, EmptyBufferIsSafe) {
|
||||
const std::string input("test");
|
||||
EXPECT_STREQ(json_escape_into_buffer(std::span<char>(), StringRef(input.c_str(), input.size())), "");
|
||||
}
|
||||
|
||||
} // namespace esphome::captive_portal::testing
|
||||
} // namespace esphome::testing
|
||||
Reference in New Issue
Block a user