mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
[core] Replace base64 lookup tables with arithmetic mapping (#18454)
This commit is contained in:
@@ -88,9 +88,17 @@ std::string str_sprintf(const char *fmt, ...) {
|
||||
|
||||
// --- Base64 helpers ---
|
||||
|
||||
static constexpr const char *BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
"abcdefghijklmnopqrstuvwxyz"
|
||||
"0123456789+/";
|
||||
// Map a 6-bit value (0-63) to its base64 character arithmetically.
|
||||
// No lookup table: a table would occupy RAM on ESP8266 (.rodata lives in DRAM there).
|
||||
static inline char base64_char(uint8_t index) {
|
||||
if (index < 26)
|
||||
return 'A' + index;
|
||||
if (index < 52)
|
||||
return 'a' + (index - 26);
|
||||
if (index < 62)
|
||||
return '0' + (index - 52);
|
||||
return index == 62 ? '+' : '/';
|
||||
}
|
||||
|
||||
// Encode 3 input bytes to 4 base64 characters, append 'count' to ret.
|
||||
static inline void base64_encode_triple(const char *char_array_3, int count, std::string &ret) {
|
||||
@@ -101,7 +109,7 @@ static inline void base64_encode_triple(const char *char_array_3, int count, std
|
||||
char_array_4[3] = char_array_3[2] & 0x3f;
|
||||
|
||||
for (int j = 0; j < count; j++)
|
||||
ret += BASE64_CHARS[static_cast<uint8_t>(char_array_4[j])];
|
||||
ret += base64_char(static_cast<uint8_t>(char_array_4[j]));
|
||||
}
|
||||
|
||||
std::string base64_encode(const std::vector<uint8_t> &buf) { return base64_encode(buf.data(), buf.size()); }
|
||||
|
||||
+12
-12
@@ -579,13 +579,8 @@ int8_t step_to_accuracy_decimals(float step) {
|
||||
return str.length() - dot_pos - 1;
|
||||
}
|
||||
|
||||
// Use C-style string constant to store in ROM instead of RAM (saves 24 bytes)
|
||||
static constexpr const char *BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
"abcdefghijklmnopqrstuvwxyz"
|
||||
"0123456789+/";
|
||||
|
||||
// Helper function to find the index of a base64/base64url character in the lookup table.
|
||||
// Returns the character's position (0-63) if found, or 0 if not found.
|
||||
// Map a base64/base64url character to its 6-bit value (0-63) arithmetically.
|
||||
// No lookup table: a table would occupy RAM on ESP8266 (.rodata lives in DRAM there).
|
||||
// Supports both standard base64 (+/) and base64url (-_) alphabets.
|
||||
// NOTE: This returns 0 for both 'A' (valid base64 char at index 0) and invalid characters.
|
||||
// This is safe because is_base64() is ALWAYS checked before calling this function,
|
||||
@@ -593,13 +588,18 @@ static constexpr const char *BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
// stops processing at the first invalid character due to the is_base64() check in its
|
||||
// while loop condition, making this edge case harmless in practice.
|
||||
static inline uint8_t base64_find_char(char c) {
|
||||
// Handle base64url variants: '-' maps to '+' (index 62), '_' maps to '/' (index 63)
|
||||
if (c == '-')
|
||||
if (c >= 'A' && c <= 'Z')
|
||||
return c - 'A';
|
||||
if (c >= 'a' && c <= 'z')
|
||||
return c - 'a' + 26;
|
||||
if (c >= '0' && c <= '9')
|
||||
return c - '0' + 52;
|
||||
// base64url variants: '-' maps to '+' (index 62), '_' maps to '/' (index 63)
|
||||
if (c == '+' || c == '-')
|
||||
return 62;
|
||||
if (c == '_')
|
||||
if (c == '/' || c == '_')
|
||||
return 63;
|
||||
const char *pos = strchr(BASE64_CHARS, c);
|
||||
return pos ? (pos - BASE64_CHARS) : 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Check if character is valid base64 or base64url
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/alloc_helpers.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::core::testing {
|
||||
@@ -213,4 +214,70 @@ TEST(BufAppendSepStr, Truncation) {
|
||||
EXPECT_EQ(end - buf, 7);
|
||||
}
|
||||
|
||||
// --- base64 encode/decode ---
|
||||
|
||||
static const char BASE64_ALPHABET[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
// Pack 6-bit indices 0..63 into 48 bytes so encoding yields the full alphabet in order
|
||||
TEST(Base64, EncodeProducesCanonicalAlphabet) {
|
||||
uint8_t bytes[48];
|
||||
size_t n = 0;
|
||||
for (uint8_t i = 0; i < 64; i += 4) {
|
||||
bytes[n++] = (i << 2) | ((i + 1) >> 4);
|
||||
bytes[n++] = ((i + 1) & 0x0F) << 4 | ((i + 2) >> 2);
|
||||
bytes[n++] = ((i + 2) & 0x03) << 6 | (i + 3);
|
||||
}
|
||||
std::string encoded = base64_encode(bytes, sizeof(bytes)); // NOLINT(esphome-heap-allocation) - host test
|
||||
EXPECT_EQ(encoded, BASE64_ALPHABET);
|
||||
}
|
||||
|
||||
// Decode the alphabet then re-encode: locks the encode and decode mappings together
|
||||
TEST(Base64, DecodeCanonicalAlphabetRoundTrip) {
|
||||
uint8_t buf[48];
|
||||
size_t len = base64_decode(std::string(BASE64_ALPHABET), buf, sizeof(buf));
|
||||
EXPECT_EQ(len, 48u);
|
||||
std::string reencoded = base64_encode(buf, len); // NOLINT(esphome-heap-allocation) - host test
|
||||
EXPECT_EQ(reencoded, BASE64_ALPHABET);
|
||||
}
|
||||
|
||||
TEST(Base64, DecodeBase64UrlMatchesStandard) {
|
||||
std::string url = BASE64_ALPHABET;
|
||||
for (char &c : url) {
|
||||
if (c == '+')
|
||||
c = '-';
|
||||
if (c == '/')
|
||||
c = '_';
|
||||
}
|
||||
uint8_t standard[48], urlsafe[48];
|
||||
size_t len_standard = base64_decode(std::string(BASE64_ALPHABET), standard, sizeof(standard));
|
||||
size_t len_url = base64_decode(url, urlsafe, sizeof(urlsafe));
|
||||
EXPECT_EQ(len_standard, len_url);
|
||||
EXPECT_EQ(memcmp(standard, urlsafe, len_standard), 0);
|
||||
}
|
||||
|
||||
// RFC 4648 vectors cover both padding cases (len % 3 == 1 and len % 3 == 2)
|
||||
TEST(Base64, Rfc4648Vectors) {
|
||||
const struct {
|
||||
const char *plain;
|
||||
const char *encoded;
|
||||
} vectors[] = {
|
||||
{"", ""},
|
||||
{"f", "Zg=="},
|
||||
{"fo", "Zm8="},
|
||||
{"foo", "Zm9v"},
|
||||
{"foob", "Zm9vYg=="},
|
||||
{"fooba", "Zm9vYmE="},
|
||||
{"foobar", "Zm9vYmFy"},
|
||||
};
|
||||
for (const auto &v : vectors) {
|
||||
const auto *plain = reinterpret_cast<const uint8_t *>(v.plain);
|
||||
std::string encoded = base64_encode(plain, strlen(v.plain)); // NOLINT(esphome-heap-allocation) - host test
|
||||
EXPECT_EQ(encoded, v.encoded);
|
||||
uint8_t buf[8];
|
||||
size_t len = base64_decode(reinterpret_cast<const uint8_t *>(v.encoded), strlen(v.encoded), buf, sizeof(buf));
|
||||
EXPECT_EQ(len, strlen(v.plain));
|
||||
EXPECT_EQ(memcmp(buf, v.plain, len), 0);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::core::testing
|
||||
|
||||
Reference in New Issue
Block a user