Skip to content

Commit 42ff73f

Browse files
Copilotnetmindz
andcommitted
Implement metadata-based OTA release checking system
Co-authored-by: netmindz <442066+netmindz@users.noreply.github.com>
1 parent 2d8edfc commit 42ff73f

File tree

4 files changed

+198
-204
lines changed

4 files changed

+198
-204
lines changed

pio-scripts/output_bins.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,37 @@ 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+
2152
def create_release(source):
2253
release_name_def = _get_cpp_define_value(env, "WLED_RELEASE_NAME")
2354
if release_name_def:
@@ -27,6 +58,10 @@ def create_release(source):
2758
release_gz_file = release_file + ".gz"
2859
print(f"Copying {source} to {release_file}")
2960
shutil.copy(source, release_file)
61+
62+
# Add metadata header to the release binary
63+
add_metadata_header(release_file, release_name)
64+
3065
bin_gzip(release_file, release_gz_file)
3166
else:
3267
variant = env["PIOENV"]

wled00/ota_release_check.cpp

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

4-
// Maximum size to scan in binary (we don't need to scan the entire file)
5-
#define MAX_SCAN_SIZE 32768
6-
7-
/**
8-
* Find a string in binary data
9-
*/
10-
static int findStringInBinary(const uint8_t* data, size_t dataSize, const char* pattern) {
11-
size_t patternLen = strlen(pattern);
12-
if (patternLen == 0 || patternLen > dataSize) return -1;
13-
14-
for (size_t i = 0; i <= dataSize - patternLen; i++) {
15-
if (memcmp(data + i, pattern, patternLen) == 0) {
16-
return i;
17-
}
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;
188
}
19-
return -1;
20-
}
219

22-
/**
23-
* Check if a string looks like a valid WLED release name
24-
*/
25-
static bool isValidReleaseNameFormat(const char* name, size_t len) {
26-
if (len < 3 || len > 63) return false;
27-
28-
// Should contain at least one letter and be mostly alphanumeric with underscores/dashes
29-
bool hasLetter = false;
30-
for (size_t i = 0; i < len; i++) {
31-
char c = name[i];
32-
if (isalpha(c)) {
33-
hasLetter = true;
34-
} else if (!isdigit(c) && c != '_' && c != '-') {
35-
return false; // Invalid character
36-
}
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;
3716
}
38-
39-
return hasLetter; // Must have at least one letter
40-
}
4117

42-
/**
43-
* Extract release name by searching for any reasonable string that could be a release name
44-
* This method is very permissive to handle custom builds and unknown release formats
45-
*/
46-
static bool extractByGenericStringSearch(const uint8_t* data, size_t dataSize, char* extractedRelease) {
47-
// Search for null-terminated strings that could be release names
48-
char bestCandidate[64] = "";
49-
int bestScore = -1;
50-
51-
for (size_t i = 0; i < dataSize - 4; i++) {
52-
// Look for potential start of a string (printable character)
53-
if (isalpha(data[i])) {
54-
// Find the end of this string (null terminator)
55-
size_t j = i;
56-
while (j < dataSize && data[j] != 0) {
57-
j++;
58-
}
59-
60-
if (j < dataSize) { // Found null terminator
61-
size_t len = j - i;
62-
if (len >= 3 && len <= 63) { // reasonable length for a release name
63-
char candidate[64];
64-
strncpy(candidate, (const char*)(data + i), len);
65-
candidate[len] = '\0';
66-
67-
// Check if this looks like a valid release name format
68-
if (isValidReleaseNameFormat(candidate, len)) {
69-
// Score candidates to find the most likely release name
70-
int score = 0;
71-
72-
// High score for common patterns
73-
if (strstr(candidate, "ESP") != NULL) score += 100;
74-
if (strstr(candidate, "WLED") != NULL) score += 100;
75-
if (strstr(candidate, "Custom") != NULL) score += 50;
76-
if (strstr(candidate, "Build") != NULL) score += 30;
77-
78-
// Medium score for reasonable structure
79-
if (len >= 5 && len <= 32) score += 20; // reasonable length
80-
if (strchr(candidate, '_') != NULL) score += 10; // contains underscore (common in release names)
81-
if (strchr(candidate, '-') != NULL) score += 10; // contains dash (common in release names)
82-
83-
// Basic score for any valid format
84-
if (score == 0) score = 5; // Any valid format gets minimum score
85-
86-
if (score > bestScore) {
87-
bestScore = score;
88-
strcpy(bestCandidate, candidate);
89-
}
90-
}
91-
}
92-
}
93-
}
94-
}
95-
96-
if (bestScore > 0) {
97-
strcpy(extractedRelease, bestCandidate);
98-
DEBUG_PRINTF_P(PSTR("Found release name by generic search (score %d): %s\n"), bestScore, extractedRelease);
99-
return true;
100-
}
101-
102-
return false;
103-
}
18+
DEBUG_PRINTLN(F("Found WLED metadata header"));
10419

105-
bool extractReleaseNameFromBinary(const uint8_t* binaryData, size_t dataSize, char* extractedRelease) {
106-
if (!binaryData || !extractedRelease || dataSize == 0) {
107-
return false;
108-
}
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;
10923

110-
// Limit scan size to avoid performance issues with large binaries
111-
size_t scanSize = (dataSize > MAX_SCAN_SIZE) ? MAX_SCAN_SIZE : dataSize;
112-
113-
// First, try to find the exact current release string in the binary
114-
// This is the most reliable method since we know what we're looking for
115-
int pos = findStringInBinary(binaryData, scanSize, releaseString);
116-
if (pos >= 0) {
117-
// Verify it's properly null-terminated
118-
size_t releaseLen = strlen(releaseString);
119-
if (pos + releaseLen < scanSize && binaryData[pos + releaseLen] == 0) {
120-
strcpy(extractedRelease, releaseString);
121-
DEBUG_PRINTF_P(PSTR("Found exact current release string in binary: %s\n"), extractedRelease);
122-
return true;
123-
}
124-
}
125-
126-
// Fallback: Search for any string that looks like a release name
127-
// This handles the case where the binary has a different but valid release name
128-
if (extractByGenericStringSearch(binaryData, scanSize, extractedRelease)) {
129-
return true;
130-
}
131-
132-
DEBUG_PRINTLN(F("Could not extract release name from binary"));
133-
return false;
24+
// Copy release name (it should be null-terminated within the header)
25+
strncpy(extractedRelease, releaseStart, maxReleaseLen);
26+
extractedRelease[maxReleaseLen] = '\0'; // Ensure null termination
27+
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;
32+
33+
DEBUG_PRINTF_P(PSTR("Extracted release name from metadata: '%s', firmware size: %zu bytes\n"),
34+
extractedRelease, firmwareSize);
35+
36+
return true;
13437
}
13538

13639
bool validateReleaseCompatibility(const char* extractedRelease) {
@@ -147,29 +50,41 @@ bool validateReleaseCompatibility(const char* extractedRelease) {
14750
return match;
14851
}
14952

150-
bool shouldAllowOTA(const uint8_t* binaryData, size_t dataSize, bool ignoreReleaseCheck, char* errorMessage) {
53+
bool shouldAllowOTA(uint8_t* binaryData, size_t dataSize, bool ignoreReleaseCheck, char* errorMessage, size_t* actualBinarySize) {
15154
// Clear error message
15255
if (errorMessage) {
15356
errorMessage[0] = '\0';
15457
}
15558

59+
// Initialize actual binary size to full size by default
60+
if (actualBinarySize) {
61+
*actualBinarySize = dataSize;
62+
}
63+
15664
// If user chose to ignore release check, allow OTA
15765
if (ignoreReleaseCheck) {
15866
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);
15970
return true;
16071
}
161-
162-
// Try to extract release name from binary
72+
73+
// Try to extract metadata header
16374
char extractedRelease[64];
164-
if (!extractReleaseNameFromBinary(binaryData, dataSize, extractedRelease)) {
75+
bool hasMetadata = extractMetadataHeader(binaryData, dataSize, extractedRelease, actualBinarySize);
76+
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
16580
if (errorMessage) {
166-
strcpy(errorMessage, "Could not determine release type of uploaded file. Check 'Ignore release name check' to proceed.");
81+
strcpy(errorMessage, "Binary has no release compatibility metadata. Check 'Ignore release name check' to proceed.");
16782
}
168-
DEBUG_PRINTLN(F("OTA blocked: Could not extract release name"));
83+
DEBUG_PRINTLN(F("OTA blocked: No metadata header found"));
16984
return false;
17085
}
171-
172-
// Validate compatibility
86+
87+
// Validate compatibility using extracted release name
17388
if (!validateReleaseCompatibility(extractedRelease)) {
17489
if (errorMessage) {
17590
snprintf(errorMessage, 127, "Release mismatch: current='%s', uploaded='%s'. Check 'Ignore release name check' to proceed.",
@@ -179,7 +94,7 @@ bool shouldAllowOTA(const uint8_t* binaryData, size_t dataSize, bool ignoreRelea
17994
releaseString, extractedRelease);
18095
return false;
18196
}
182-
97+
18398
DEBUG_PRINTLN(F("OTA allowed: Release names match"));
18499
return true;
185100
}

wled00/ota_release_check.h

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

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

99
#include <Arduino.h>
1010

11+
#define WLED_META_HEADER_SIZE 64
12+
#define WLED_META_PREFIX "WLED_META:"
13+
1114
/**
12-
* Extract release name from ESP32/ESP8266 binary data
13-
* @param binaryData Pointer to binary file data
15+
* Extract and remove metadata header from binary data
16+
* @param binaryData Pointer to binary file data (will be modified)
1417
* @param dataSize Size of binary data in bytes
1518
* @param extractedRelease Buffer to store extracted release name (should be at least 64 bytes)
16-
* @return true if release name was extracted successfully, false otherwise
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
1721
*/
18-
bool extractReleaseNameFromBinary(const uint8_t* binaryData, size_t dataSize, char* extractedRelease);
22+
bool extractMetadataHeader(uint8_t* binaryData, size_t dataSize, char* extractedRelease, size_t* actualBinarySize);
1923

2024
/**
2125
* Validate if extracted release name matches current release
@@ -25,13 +29,14 @@ bool extractReleaseNameFromBinary(const uint8_t* binaryData, size_t dataSize, ch
2529
bool validateReleaseCompatibility(const char* extractedRelease);
2630

2731
/**
28-
* Check if OTA should be allowed based on release compatibility
29-
* @param binaryData Pointer to binary file data
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)
3034
* @param dataSize Size of binary data in bytes
3135
* @param ignoreReleaseCheck If true, skip release validation
3236
* @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)
3338
* @return true if OTA should proceed, false if it should be blocked
3439
*/
35-
bool shouldAllowOTA(const uint8_t* binaryData, size_t dataSize, bool ignoreReleaseCheck, char* errorMessage);
40+
bool shouldAllowOTA(uint8_t* binaryData, size_t dataSize, bool ignoreReleaseCheck, char* errorMessage, size_t* actualBinarySize);
3641

3742
#endif // WLED_OTA_RELEASE_CHECK_H

0 commit comments

Comments
 (0)