From 4d4498e81f5da5ab8dfd4396224d26ff118f1f48 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Dec 2025 14:57:42 -1000 Subject: [PATCH 1/4] fix max --- esphome/core/helpers.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index ce865d11a0..66e1fe1512 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -312,10 +312,10 @@ char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data buffer[0] = '\0'; return buffer; } - // With separator: each byte needs 3 chars (XX + sep), last byte needs 2 + null = length*3 - // Without separator: each byte needs 2 chars + null = length*2 + 1 + // With separator: total length is 3*length (2*length hex chars, (length-1) separators, 1 null terminator) + // Without separator: total length is 2*length + 1 (2*length hex chars, 1 null terminator) uint8_t stride = separator ? 3 : 2; - size_t max_bytes = (buffer_size - 1) / stride + (separator ? 1 : 0); + size_t max_bytes = separator ? (buffer_size / stride) : ((buffer_size - 1) / stride); if (max_bytes == 0) { buffer[0] = '\0'; return buffer; From 38850a9ab35273fe09875c81d9e82c47b8ec7789 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Dec 2025 15:08:44 -1000 Subject: [PATCH 2/4] more dry --- esphome/core/helpers.cpp | 45 +++++++++++++++++++--------------------- esphome/core/helpers.h | 8 ++++--- 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 66e1fe1512..1c68f1a021 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -286,28 +286,9 @@ std::string format_mac_address_pretty(const uint8_t *mac) { return std::string(buf); } -char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length) { - size_t max_bytes = (buffer_size - 1) / 2; - if (length > max_bytes) { - length = max_bytes; - } - for (size_t i = 0; i < length; i++) { - buffer[2 * i] = format_hex_char(data[i] >> 4); - buffer[2 * i + 1] = format_hex_char(data[i] & 0x0F); - } - buffer[length * 2] = '\0'; - return buffer; -} - -std::string format_hex(const uint8_t *data, size_t length) { - std::string ret; - ret.resize(length * 2); - format_hex_to(&ret[0], length * 2 + 1, data, length); - return ret; -} -std::string format_hex(const std::vector &data) { return format_hex(data.data(), data.size()); } - -char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator) { +// Internal helper for hex formatting - base is 'a' for lowercase or 'A' for uppercase +static char *format_hex_internal(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator, + char base) { if (length == 0) { buffer[0] = '\0'; return buffer; @@ -325,8 +306,8 @@ char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data } for (size_t i = 0; i < length; i++) { size_t pos = i * stride; - buffer[pos] = format_hex_pretty_char(data[i] >> 4); - buffer[pos + 1] = format_hex_pretty_char(data[i] & 0x0F); + buffer[pos] = format_hex_char(data[i] >> 4, base); + buffer[pos + 1] = format_hex_char(data[i] & 0x0F, base); if (separator && i < length - 1) { buffer[pos + 2] = separator; } @@ -335,6 +316,22 @@ char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data return buffer; } +char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length) { + return format_hex_internal(buffer, buffer_size, data, length, 0, 'a'); +} + +std::string format_hex(const uint8_t *data, size_t length) { + std::string ret; + ret.resize(length * 2); + format_hex_to(&ret[0], length * 2 + 1, data, length); + return ret; +} +std::string format_hex(const std::vector &data) { return format_hex(data.data(), data.size()); } + +char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator) { + return format_hex_internal(buffer, buffer_size, data, length, separator, 'A'); +} + // Shared implementation for uint8_t and string hex formatting static std::string format_hex_pretty_uint8(const uint8_t *data, size_t length, char separator, bool show_length) { if (data == nullptr || length == 0) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index fbd3aefd59..37534849d0 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -677,12 +677,14 @@ constexpr uint8_t parse_hex_char(char c) { return 255; } +/// Convert a nibble (0-15) to hex char with specified base ('a' for lowercase, 'A' for uppercase) +inline char format_hex_char(uint8_t v, char base) { return v >= 10 ? base + (v - 10) : '0' + v; } + /// Convert a nibble (0-15) to lowercase hex char -inline char format_hex_char(uint8_t v) { return v >= 10 ? 'a' + (v - 10) : '0' + v; } +inline char format_hex_char(uint8_t v) { return format_hex_char(v, 'a'); } /// Convert a nibble (0-15) to uppercase hex char (used for pretty printing) -/// This always uses uppercase (A-F) for pretty/human-readable output -inline char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; } +inline char format_hex_pretty_char(uint8_t v) { return format_hex_char(v, 'A'); } /// Write int8 value to buffer without modulo operations. /// Buffer must have at least 4 bytes free. Returns pointer past last char written. From 89f326be30098acb063c76f822d36fb249341615 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Dec 2025 15:12:30 -1000 Subject: [PATCH 3/4] reduce --- esphome/core/hash_base.h | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/esphome/core/hash_base.h b/esphome/core/hash_base.h index c45c4df70b..cc4fcdf920 100644 --- a/esphome/core/hash_base.h +++ b/esphome/core/hash_base.h @@ -26,13 +26,7 @@ class HashBase { void get_bytes(uint8_t *output) { memcpy(output, this->digest_, this->get_size()); } /// Retrieve the hash as hex characters - void get_hex(char *output) { - for (size_t i = 0; i < this->get_size(); i++) { - uint8_t byte = this->digest_[i]; - output[i * 2] = format_hex_char(byte >> 4); - output[i * 2 + 1] = format_hex_char(byte & 0x0F); - } - } + void get_hex(char *output) { format_hex_to(output, this->get_size() * 2 + 1, this->digest_, this->get_size()); } /// Compare the hash against a provided byte-encoded hash bool equals_bytes(const uint8_t *expected) { return memcmp(this->digest_, expected, this->get_size()) == 0; } From 05c51b6ced1c5acce1e9a10aa6765583c00905af Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 27 Dec 2025 15:18:47 -1000 Subject: [PATCH 4/4] Add isolated tests for hex formatting functions --- tests/test_helpers_hex_formatting.cpp | 212 ++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 tests/test_helpers_hex_formatting.cpp diff --git a/tests/test_helpers_hex_formatting.cpp b/tests/test_helpers_hex_formatting.cpp new file mode 100644 index 0000000000..4c39ff6913 --- /dev/null +++ b/tests/test_helpers_hex_formatting.cpp @@ -0,0 +1,212 @@ +#include +#include +#include +#include + +// Copy the implementations to test them in isolation + +inline char format_hex_char(uint8_t v, char base) { return v >= 10 ? base + (v - 10) : '0' + v; } +inline char format_hex_char(uint8_t v) { return format_hex_char(v, 'a'); } +inline char format_hex_pretty_char(uint8_t v) { return format_hex_char(v, 'A'); } + +constexpr size_t format_hex_pretty_size(size_t byte_count) { return byte_count * 3; } + +static char *format_hex_internal(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator, + char base) { + if (length == 0) { + buffer[0] = '\0'; + return buffer; + } + uint8_t stride = separator ? 3 : 2; + size_t max_bytes = separator ? (buffer_size / stride) : ((buffer_size - 1) / stride); + if (max_bytes == 0) { + buffer[0] = '\0'; + return buffer; + } + if (length > max_bytes) { + length = max_bytes; + } + for (size_t i = 0; i < length; i++) { + size_t pos = i * stride; + buffer[pos] = format_hex_char(data[i] >> 4, base); + buffer[pos + 1] = format_hex_char(data[i] & 0x0F, base); + if (separator && i < length - 1) { + buffer[pos + 2] = separator; + } + } + buffer[length * stride - (separator ? 1 : 0)] = '\0'; + return buffer; +} + +char *format_hex_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length) { + return format_hex_internal(buffer, buffer_size, data, length, 0, 'a'); +} + +char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator) { + return format_hex_internal(buffer, buffer_size, data, length, separator, 'A'); +} + +template +char *format_hex_pretty_to(char (&buffer)[N], const uint8_t *data, size_t length, char separator = ':') { + return format_hex_pretty_to(buffer, N, data, length, separator); +} + +static constexpr size_t MAC_ADDRESS_SIZE = 6; +static constexpr size_t MAC_ADDRESS_PRETTY_BUFFER_SIZE = format_hex_pretty_size(MAC_ADDRESS_SIZE); +static constexpr size_t MAC_ADDRESS_BUFFER_SIZE = MAC_ADDRESS_SIZE * 2 + 1; + +inline void format_mac_addr_upper(const uint8_t *mac, char *output) { + format_hex_pretty_to(output, MAC_ADDRESS_PRETTY_BUFFER_SIZE, mac, MAC_ADDRESS_SIZE, ':'); +} + +inline void format_mac_addr_lower_no_sep(const uint8_t *mac, char *output) { + format_hex_to(output, MAC_ADDRESS_BUFFER_SIZE, mac, MAC_ADDRESS_SIZE); +} + +// Tests + +void test_format_hex_char_base() { + assert(format_hex_char(0, 'a') == '0'); + assert(format_hex_char(9, 'a') == '9'); + assert(format_hex_char(10, 'a') == 'a'); + assert(format_hex_char(15, 'a') == 'f'); + assert(format_hex_char(0, 'A') == '0'); + assert(format_hex_char(10, 'A') == 'A'); + assert(format_hex_char(15, 'A') == 'F'); + printf("✓ format_hex_char with base\n"); +} + +void test_format_hex_char_lowercase() { + assert(format_hex_char(0) == '0'); + assert(format_hex_char(10) == 'a'); + assert(format_hex_char(15) == 'f'); + printf("✓ format_hex_char lowercase\n"); +} + +void test_format_hex_pretty_char_uppercase() { + assert(format_hex_pretty_char(0) == '0'); + assert(format_hex_pretty_char(10) == 'A'); + assert(format_hex_pretty_char(15) == 'F'); + printf("✓ format_hex_pretty_char uppercase\n"); +} + +void test_format_hex_to() { + uint8_t data[] = {0xde, 0xad, 0xbe, 0xef}; + char buf[9]; + format_hex_to(buf, sizeof(buf), data, 4); + assert(strcmp(buf, "deadbeef") == 0); + printf("✓ format_hex_to lowercase\n"); +} + +void test_format_hex_pretty_to_colon() { + uint8_t data[] = {0xDE, 0xAD, 0xBE, 0xEF}; + char buf[12]; + format_hex_pretty_to(buf, sizeof(buf), data, 4, ':'); + assert(strcmp(buf, "DE:AD:BE:EF") == 0); + printf("✓ format_hex_pretty_to with colon\n"); +} + +void test_format_hex_pretty_to_dot() { + uint8_t data[] = {0xAA, 0xBB, 0xCC}; + char buf[9]; + format_hex_pretty_to(buf, sizeof(buf), data, 3, '.'); + assert(strcmp(buf, "AA.BB.CC") == 0); + printf("✓ format_hex_pretty_to with dot\n"); +} + +void test_buffer_overflow_protection() { + uint8_t data[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE}; + char buf[11]; + format_hex_pretty_to(buf, 11, data, 5, ':'); + assert(strcmp(buf, "AA:BB:CC") == 0); + printf("✓ buffer overflow protection\n"); +} + +void test_exact_fit() { + uint8_t data[] = {0xAA, 0xBB, 0xCC, 0xDD}; + char buf[12]; + format_hex_pretty_to(buf, 12, data, 4, ':'); + assert(strcmp(buf, "AA:BB:CC:DD") == 0); + printf("✓ exact fit buffer\n"); +} + +void test_mac_addr_upper() { + uint8_t mac[] = {0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}; + char buf[18]; + format_mac_addr_upper(mac, buf); + assert(strcmp(buf, "AA:BB:CC:DD:EE:FF") == 0); + printf("✓ format_mac_addr_upper\n"); +} + +void test_mac_addr_lower_no_sep() { + uint8_t mac[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}; + char buf[13]; + format_mac_addr_lower_no_sep(mac, buf); + assert(strcmp(buf, "aabbccddeeff") == 0); + printf("✓ format_mac_addr_lower_no_sep\n"); +} + +void test_empty() { + uint8_t data[] = {0xAA}; + char buf[12]; + format_hex_pretty_to(buf, sizeof(buf), data, 0, ':'); + assert(strcmp(buf, "") == 0); + printf("✓ empty data\n"); +} + +void test_single_byte() { + uint8_t data[] = {0x42}; + char buf[3]; + format_hex_pretty_to(buf, sizeof(buf), data, 1, ':'); + assert(strcmp(buf, "42") == 0); + printf("✓ single byte\n"); +} + +void test_template() { + uint8_t data[] = {0xDE, 0xAD, 0xBE, 0xEF}; + char buf[format_hex_pretty_size(4)]; + format_hex_pretty_to(buf, data, 4); + assert(strcmp(buf, "DE:AD:BE:EF") == 0); + printf("✓ template version\n"); +} + +void test_no_separator() { + uint8_t data[] = {0xDE, 0xAD}; + char buf[5]; + format_hex_pretty_to(buf, sizeof(buf), data, 2, 0); + assert(strcmp(buf, "DEAD") == 0); + printf("✓ no separator\n"); +} + +void test_constexpr() { + static_assert(format_hex_pretty_size(1) == 3, ""); + static_assert(format_hex_pretty_size(4) == 12, ""); + static_assert(format_hex_pretty_size(6) == 18, ""); + static_assert(MAC_ADDRESS_SIZE == 6, ""); + static_assert(MAC_ADDRESS_PRETTY_BUFFER_SIZE == 18, ""); + static_assert(MAC_ADDRESS_BUFFER_SIZE == 13, ""); + printf("✓ constexpr values\n"); +} + +int main() { + printf("Running hex formatting tests...\n\n"); + + test_format_hex_char_base(); + test_format_hex_char_lowercase(); + test_format_hex_pretty_char_uppercase(); + test_format_hex_to(); + test_format_hex_pretty_to_colon(); + test_format_hex_pretty_to_dot(); + test_buffer_overflow_protection(); + test_exact_fit(); + test_mac_addr_upper(); + test_mac_addr_lower_no_sep(); + test_empty(); + test_single_byte(); + test_template(); + test_no_separator(); + test_constexpr(); + + printf("\n✅ All 15 tests passed!\n"); + return 0; +}