From d0d8f2935d5b61485b7cf6c3bd68b28a198e44f1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 30 Mar 2026 21:45:34 -1000 Subject: [PATCH] [time] Handle %%Z/%%z correctly in strftime format substitution --- esphome/core/time.cpp | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/esphome/core/time.cpp b/esphome/core/time.cpp index 3f379525bab..46e38fb3b64 100644 --- a/esphome/core/time.cpp +++ b/esphome/core/time.cpp @@ -21,12 +21,17 @@ size_t ESPTime::strftime(char *buffer, size_t buffer_len, const char *format) { // ::strftime uses libc's internal timezone state for %Z and %z, but we // eliminated setenv("TZ")/tzset() on embedded platforms to save flash. // Substitute %Z and %z with correct values from our parsed timezone. - // Quick scan: does format contain %Z or %z? + // Quick scan: does format contain %Z or %z (but not %%Z/%%z)? bool needs_subst = false; for (const char *p = format; *p; p++) { - if (*p == '%' && (*(p + 1) == 'Z' || *(p + 1) == 'z')) { - needs_subst = true; - break; + if (*p == '%') { + p++; + if (*p == '%') + continue; // %% is a literal %, skip + if (*p == 'Z' || *p == 'z') { + needs_subst = true; + break; + } } } if (needs_subst) { @@ -39,10 +44,19 @@ size_t ESPTime::strftime(char *buffer, size_t buffer_len, const char *format) { char *out = modified; char *out_end = modified + sizeof(modified) - 1; for (const char *p = format; *p && out < out_end; p++) { - if (*p == '%' && (*(p + 1) == 'Z' || *(p + 1) == 'z')) { - p++; // skip the Z/z - for (const char *d = designation; *d && out < out_end; d++) - *out++ = *d; + if (*p == '%') { + if (*(p + 1) == '%') { + // %% → copy both percent signs (literal %) + *out++ = *p++; + if (out < out_end) + *out++ = *p; + } else if (*(p + 1) == 'Z' || *(p + 1) == 'z') { + p++; // skip the Z/z + for (const char *d = designation; *d && out < out_end; d++) + *out++ = *d; + } else { + *out++ = *p; + } } else { *out++ = *p; }