Skip to content

Commit 7d550ba

Browse files
Copilotnetmindz
andcommitted
Replace metadata header approach with ESP-IDF custom description section
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
1 parent 42ff73f commit 7d550ba

File tree

5 files changed

+135
-106
lines changed

5 files changed

+135
-106
lines changed

pio-scripts/output_bins.py

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -18,37 +18,6 @@ def _create_dirs(dirs=["map", "release", "firmware"]):
1818
for d in dirs:
1919
os.makedirs(os.path.join(OUTPUT_DIR, d), exist_ok=True)
2020

21-
def add_metadata_header(binary_file, release_name):
22-
"""Add WLED release metadata header to binary file"""
23-
# Metadata format: "WLED_META:" + release_name + null terminator + padding to 64 bytes + original binary
24-
header_prefix = b"WLED_META:"
25-
release_bytes = release_name.encode('utf-8')
26-
27-
# Ensure total header is exactly 64 bytes for alignment
28-
header_size = 64
29-
header_data_size = len(header_prefix) + len(release_bytes) + 1 # +1 for null terminator
30-
31-
if header_data_size > header_size:
32-
print(f"Warning: Release name '{release_name}' too long, truncating")
33-
max_release_len = header_size - len(header_prefix) - 1
34-
release_bytes = release_bytes[:max_release_len]
35-
header_data_size = len(header_prefix) + len(release_bytes) + 1
36-
37-
# Create header with padding
38-
header = header_prefix + release_bytes + b'\0'
39-
header += b'\xFF' * (header_size - header_data_size) # Pad with 0xFF (erased flash pattern)
40-
41-
# Read original binary
42-
with open(binary_file, 'rb') as f:
43-
original_data = f.read()
44-
45-
# Write header + original binary
46-
with open(binary_file, 'wb') as f:
47-
f.write(header)
48-
f.write(original_data)
49-
50-
print(f"Added WLED metadata header with release name '{release_name}' to {binary_file}")
51-
5221
def create_release(source):
5322
release_name_def = _get_cpp_define_value(env, "WLED_RELEASE_NAME")
5423
if release_name_def:
@@ -58,10 +27,6 @@ def create_release(source):
5827
release_gz_file = release_file + ".gz"
5928
print(f"Copying {source} to {release_file}")
6029
shutil.copy(source, release_file)
61-
62-
# Add metadata header to the release binary
63-
add_metadata_header(release_file, release_name)
64-
6530
bin_gzip(release_file, release_gz_file)
6631
else:
6732
variant = env["PIOENV"]

wled00/ota_release_check.cpp

Lines changed: 74 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,75 @@
11
#include "ota_release_check.h"
22
#include "wled.h"
33

4-
bool extractMetadataHeader(uint8_t* binaryData, size_t dataSize, char* extractedRelease, size_t* actualBinarySize) {
5-
if (!binaryData || !extractedRelease || !actualBinarySize || dataSize < WLED_META_HEADER_SIZE) {
6-
*actualBinarySize = dataSize;
7-
return false;
8-
}
4+
#ifdef ESP32
5+
#include <esp_app_format.h>
6+
#include <esp_ota_ops.h>
7+
#endif
98

10-
// Check if the binary starts with our metadata header
11-
if (memcmp(binaryData, WLED_META_PREFIX, strlen(WLED_META_PREFIX)) != 0) {
12-
// No metadata header found, this is a legacy binary
13-
*actualBinarySize = dataSize;
14-
DEBUG_PRINTLN(F("No WLED metadata header found - legacy binary"));
15-
return false;
16-
}
9+
bool extractReleaseFromCustomDesc(const uint8_t* binaryData, size_t dataSize, char* extractedRelease) {
10+
if (!binaryData || !extractedRelease || dataSize < sizeof(esp_image_header_t)) {
11+
return false;
12+
}
1713

18-
DEBUG_PRINTLN(F("Found WLED metadata header"));
14+
#ifdef ESP32
15+
// Look for ESP32 image header to find the custom description section
16+
const esp_image_header_t* header = (const esp_image_header_t*)binaryData;
17+
18+
// Validate ESP32 image header
19+
if (header->magic != ESP_IMAGE_HEADER_MAGIC) {
20+
DEBUG_PRINTLN(F("Not a valid ESP32 image - missing magic header"));
21+
return false;
22+
}
1923

20-
// Extract release name from header
21-
const char* releaseStart = (const char*)(binaryData + strlen(WLED_META_PREFIX));
22-
size_t maxReleaseLen = WLED_META_HEADER_SIZE - strlen(WLED_META_PREFIX) - 1;
23-
24-
// Copy release name (it should be null-terminated within the header)
25-
strncpy(extractedRelease, releaseStart, maxReleaseLen);
26-
extractedRelease[maxReleaseLen] = '\0'; // Ensure null termination
24+
// The custom description section is located at a fixed offset after the image header
25+
// ESP-IDF places custom description at offset 0x20 in the binary for ESP32
26+
const size_t custom_desc_offset = 0x20;
27+
28+
if (dataSize < custom_desc_offset + sizeof(wled_custom_desc_t)) {
29+
DEBUG_PRINTLN(F("Binary too small to contain custom description"));
30+
return false;
31+
}
2732

28-
// Remove metadata header by shifting binary data
29-
size_t firmwareSize = dataSize - WLED_META_HEADER_SIZE;
30-
memmove(binaryData, binaryData + WLED_META_HEADER_SIZE, firmwareSize);
31-
*actualBinarySize = firmwareSize;
33+
const wled_custom_desc_t* custom_desc = (const wled_custom_desc_t*)(binaryData + custom_desc_offset);
34+
35+
// Validate magic number and version
36+
if (custom_desc->magic != WLED_CUSTOM_DESC_MAGIC) {
37+
DEBUG_PRINTLN(F("No WLED custom description found - no magic number"));
38+
return false;
39+
}
3240

33-
DEBUG_PRINTF_P(PSTR("Extracted release name from metadata: '%s', firmware size: %zu bytes\n"),
34-
extractedRelease, firmwareSize);
41+
if (custom_desc->version != WLED_CUSTOM_DESC_VERSION) {
42+
DEBUG_PRINTF_P(PSTR("Unsupported custom description version: %u\n"), custom_desc->version);
43+
return false;
44+
}
3545

36-
return true;
46+
// Validate simple hash checksum (using the same simple hash as in wled_custom_desc.cpp)
47+
auto simple_hash = [](const char* str) -> uint32_t {
48+
uint32_t hash = 5381;
49+
for (int i = 0; str[i]; ++i) {
50+
hash = ((hash << 5) + hash) + str[i];
51+
}
52+
return hash;
53+
};
54+
55+
uint32_t expected_hash = simple_hash(custom_desc->release_name);
56+
if (custom_desc->crc32 != expected_hash) {
57+
DEBUG_PRINTF_P(PSTR("Custom description hash mismatch: expected 0x%08x, got 0x%08x\n"),
58+
expected_hash, custom_desc->crc32);
59+
return false;
60+
}
61+
62+
// Extract release name (ensure null termination)
63+
strncpy(extractedRelease, custom_desc->release_name, WLED_RELEASE_NAME_MAX_LEN - 1);
64+
extractedRelease[WLED_RELEASE_NAME_MAX_LEN - 1] = '\0';
65+
66+
DEBUG_PRINTF_P(PSTR("Extracted release name from custom description: '%s'\n"), extractedRelease);
67+
return true;
68+
#else
69+
// ESP8266 doesn't use ESP-IDF format, so we can't extract custom description
70+
DEBUG_PRINTLN(F("ESP8266 binaries do not support custom description extraction"));
71+
return false;
72+
#endif
3773
}
3874

3975
bool validateReleaseCompatibility(const char* extractedRelease) {
@@ -50,37 +86,32 @@ bool validateReleaseCompatibility(const char* extractedRelease) {
5086
return match;
5187
}
5288

53-
bool shouldAllowOTA(uint8_t* binaryData, size_t dataSize, bool ignoreReleaseCheck, char* errorMessage, size_t* actualBinarySize) {
89+
bool shouldAllowOTA(const uint8_t* binaryData, size_t dataSize, bool ignoreReleaseCheck, char* errorMessage) {
5490
// Clear error message
5591
if (errorMessage) {
5692
errorMessage[0] = '\0';
5793
}
58-
59-
// Initialize actual binary size to full size by default
60-
if (actualBinarySize) {
61-
*actualBinarySize = dataSize;
62-
}
6394

6495
// If user chose to ignore release check, allow OTA
6596
if (ignoreReleaseCheck) {
6697
DEBUG_PRINTLN(F("OTA release check bypassed by user"));
67-
// Still need to extract metadata header if present to get clean binary
68-
char dummyRelease[64];
69-
extractMetadataHeader(binaryData, dataSize, dummyRelease, actualBinarySize);
7098
return true;
7199
}
72100

73-
// Try to extract metadata header
74-
char extractedRelease[64];
75-
bool hasMetadata = extractMetadataHeader(binaryData, dataSize, extractedRelease, actualBinarySize);
101+
// Try to extract release name from custom description section
102+
char extractedRelease[WLED_RELEASE_NAME_MAX_LEN];
103+
bool hasCustomDesc = extractReleaseFromCustomDesc(binaryData, dataSize, extractedRelease);
76104

77-
if (!hasMetadata) {
78-
// No metadata header - this could be a legacy binary or a binary without our metadata
79-
// We cannot determine compatibility for such binaries
105+
if (!hasCustomDesc) {
106+
// No custom description - this could be a legacy binary or ESP8266 binary
80107
if (errorMessage) {
108+
#ifdef ESP32
81109
strcpy(errorMessage, "Binary has no release compatibility metadata. Check 'Ignore release name check' to proceed.");
110+
#else
111+
strcpy(errorMessage, "ESP8266 binaries do not support release checking. Check 'Ignore release name check' to proceed.");
112+
#endif
82113
}
83-
DEBUG_PRINTLN(F("OTA blocked: No metadata header found"));
114+
DEBUG_PRINTLN(F("OTA blocked: No custom description found"));
84115
return false;
85116
}
86117

wled00/ota_release_check.h

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,41 @@
22
#define WLED_OTA_RELEASE_CHECK_H
33

44
/*
5-
* OTA Release Compatibility Checking with Metadata Headers
6-
* Functions to extract and validate release names from uploaded binary files with metadata headers
5+
* OTA Release Compatibility Checking using ESP-IDF Custom Description Section
6+
* Functions to extract and validate release names from uploaded binary files using embedded metadata
77
*/
88

99
#include <Arduino.h>
1010

11-
#define WLED_META_HEADER_SIZE 64
12-
#define WLED_META_PREFIX "WLED_META:"
11+
#ifdef ESP32
12+
#include <esp_app_format.h>
13+
#endif
14+
15+
#define WLED_CUSTOM_DESC_MAGIC 0x57535453 // "WSTS" (WLED System Tag Structure)
16+
#define WLED_CUSTOM_DESC_VERSION 1
17+
#define WLED_RELEASE_NAME_MAX_LEN 48
18+
19+
/**
20+
* WLED Custom Description Structure
21+
* This structure is embedded in the .rodata_custom_desc section at a fixed offset
22+
* in ESP32 binaries, allowing extraction without modifying the binary format
23+
*/
24+
typedef struct {
25+
uint32_t magic; // Magic number to identify WLED custom description
26+
uint32_t version; // Structure version for future compatibility
27+
char release_name[WLED_RELEASE_NAME_MAX_LEN]; // Release name (null-terminated)
28+
uint32_t crc32; // CRC32 of the above fields for integrity check
29+
uint8_t reserved[12]; // Reserved for future use, must be zero
30+
} __attribute__((packed)) wled_custom_desc_t;
1331

1432
/**
15-
* Extract and remove metadata header from binary data
16-
* @param binaryData Pointer to binary file data (will be modified)
33+
* Extract release name from binary using ESP-IDF custom description section
34+
* @param binaryData Pointer to binary file data
1735
* @param dataSize Size of binary data in bytes
18-
* @param extractedRelease Buffer to store extracted release name (should be at least 64 bytes)
19-
* @param actualBinarySize Pointer to store the size of actual firmware binary (without header)
20-
* @return true if metadata header was found and extracted, false if no metadata header present
36+
* @param extractedRelease Buffer to store extracted release name (should be at least WLED_RELEASE_NAME_MAX_LEN bytes)
37+
* @return true if release name was found and extracted, false otherwise
2138
*/
22-
bool extractMetadataHeader(uint8_t* binaryData, size_t dataSize, char* extractedRelease, size_t* actualBinarySize);
39+
bool extractReleaseFromCustomDesc(const uint8_t* binaryData, size_t dataSize, char* extractedRelease);
2340

2441
/**
2542
* Validate if extracted release name matches current release
@@ -29,14 +46,13 @@ bool extractMetadataHeader(uint8_t* binaryData, size_t dataSize, char* extracted
2946
bool validateReleaseCompatibility(const char* extractedRelease);
3047

3148
/**
32-
* Check if OTA should be allowed based on release compatibility using metadata headers
33-
* @param binaryData Pointer to binary file data (will be modified if metadata header present)
49+
* Check if OTA should be allowed based on release compatibility using custom description
50+
* @param binaryData Pointer to binary file data (not modified)
3451
* @param dataSize Size of binary data in bytes
3552
* @param ignoreReleaseCheck If true, skip release validation
3653
* @param errorMessage Buffer to store error message if validation fails (should be at least 128 bytes)
37-
* @param actualBinarySize Pointer to store the size of actual firmware binary (without header)
3854
* @return true if OTA should proceed, false if it should be blocked
3955
*/
40-
bool shouldAllowOTA(uint8_t* binaryData, size_t dataSize, bool ignoreReleaseCheck, char* errorMessage, size_t* actualBinarySize);
56+
bool shouldAllowOTA(const uint8_t* binaryData, size_t dataSize, bool ignoreReleaseCheck, char* errorMessage);
4157

4258
#endif // WLED_OTA_RELEASE_CHECK_H

wled00/wled_custom_desc.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#include "ota_release_check.h"
2+
#include "wled.h"
3+
4+
#ifdef ESP32
5+
6+
// Simple hash function for validation (compile-time friendly)
7+
constexpr uint32_t simple_hash(const char* str) {
8+
uint32_t hash = 5381;
9+
for (int i = 0; str[i]; ++i) {
10+
hash = ((hash << 5) + hash) + str[i];
11+
}
12+
return hash;
13+
}
14+
15+
// Create the custom description structure with current release name
16+
// This will be embedded at a fixed offset in the ESP32 binary
17+
const wled_custom_desc_t __attribute__((section(".rodata_custom_desc"))) wled_custom_description = {
18+
.magic = WLED_CUSTOM_DESC_MAGIC,
19+
.version = WLED_CUSTOM_DESC_VERSION,
20+
.release_name = WLED_RELEASE_NAME,
21+
.crc32 = simple_hash(WLED_RELEASE_NAME), // Use simple hash for validation
22+
.reserved = {0}
23+
};
24+
25+
#endif // ESP32

wled00/wled_server.cpp

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -480,8 +480,7 @@ void initServer()
480480
bool ignoreRelease = request->hasParam("ignoreRelease", true);
481481

482482
char errorMessage[128];
483-
size_t actualFirmwareSize;
484-
releaseCheckPassed = shouldAllowOTA(firstChunkBuffer, len, ignoreRelease, errorMessage, &actualFirmwareSize);
483+
releaseCheckPassed = shouldAllowOTA(firstChunkBuffer, len, ignoreRelease, errorMessage);
485484

486485
if (!releaseCheckPassed) {
487486
DEBUG_PRINTF_P(PSTR("OTA blocked: %s\n"), errorMessage);
@@ -497,15 +496,8 @@ void initServer()
497496

498497
DEBUG_PRINTLN(F("Release check passed, starting OTA update"));
499498

500-
// Calculate expected firmware size (if metadata header was present, actualFirmwareSize will be adjusted)
501-
// For final size calculation, we need to account for all chunks
499+
// Use full content length as firmware size (no binary modification with custom description approach)
502500
expectedFirmwareSize = request->contentLength();
503-
if (actualFirmwareSize < len) {
504-
// Metadata header was removed, adjust expected size
505-
size_t headerSize = len - actualFirmwareSize;
506-
expectedFirmwareSize -= headerSize;
507-
DEBUG_PRINTF_P(PSTR("Metadata header removed, firmware size: %zu bytes\n"), expectedFirmwareSize);
508-
}
509501

510502
// Start the actual OTA update
511503
strip.suspend();
@@ -530,12 +522,12 @@ void initServer()
530522
return;
531523
}
532524

533-
// Write the processed first chunk (with metadata header removed if present)
534-
if (actualFirmwareSize > 0 && !Update.hasError()) {
535-
if (Update.write(firstChunkBuffer, actualFirmwareSize) != actualFirmwareSize) {
525+
// Write the first chunk of firmware data
526+
if (len > 0 && !Update.hasError()) {
527+
if (Update.write(firstChunkBuffer, len) != len) {
536528
DEBUG_PRINTF_P(PSTR("OTA write failed on first chunk: %s\n"), Update.getErrorString().c_str());
537529
} else {
538-
totalBytesWritten += actualFirmwareSize;
530+
totalBytesWritten += len;
539531
}
540532
}
541533

0 commit comments

Comments
 (0)