App metadata filtering in app-module

This commit is contained in:
Ken Van Hoeylandt 2026-08-16 18:26:22 +02:00
parent 623ed9414c
commit e75016f5cf
4 changed files with 86 additions and 0 deletions

View File

@ -13,6 +13,8 @@ extern "C" {
#define APP_METADATA_APP_ID_LENGTH 32
#define APP_METADATA_APP_NAME_LENGTH 32
#define APP_METADATA_APP_VERSION_NAME_LENGTH 16
#define APP_METADATA_REQUIRES_DEVICE_ARCH_LENGTH 32
#define APP_METADATA_REQUIRES_DEVICE_ID_LENGTH 64
struct AppMetadata {
@ -42,6 +44,20 @@ struct AppMetadata {
/** The technical version (must be incremented with new releases of the app) */
uint64_t app_version_code;
/**
* Comma-separated list of MCU architectures the app is restricted to (e.g. "esp32s3,esp32p4"),
* matching ESP-IDF target names. Empty means unrestricted.
* Must be NULL-terminated.
*/
char requires_device_arch[APP_METADATA_REQUIRES_DEVICE_ARCH_LENGTH + 1];
/**
* Comma-separated list of device ids the app is restricted to (e.g. "m5stack-tab5"), matching
* the folder names under Devices/. Empty means unrestricted.
* Must be NULL-terminated.
*/
char requires_device_id[APP_METADATA_REQUIRES_DEVICE_ID_LENGTH + 1];
};
/**

View File

@ -18,6 +18,12 @@ bool app_metadata_is_valid_name(const std::string& name);
bool app_metadata_is_valid_version_name(const std::string& version);
bool app_metadata_is_valid_version_code(const std::string& version);
/** Validates a comma-separated list of MCU architecture names (alphanumeric items only). */
bool app_metadata_is_valid_device_arch_list(const std::string& value);
/** Validates a comma-separated list of device ids (alphanumeric + '-' items, matching Devices/<id> folder names). */
bool app_metadata_is_valid_device_id_list(const std::string& value);
/** Copies @a value into @a dest (a fixed-size buffer of @a dest_size bytes, including the NULL
* terminator) if it fits.
* @retval false @a value doesn't fit in @a dest_size bytes - @a dest is left untouched */

View File

@ -37,6 +37,26 @@ bool validate_string(const std::string& value, bool (*is_valid_char)(char)) {
return true;
}
/** Validates a comma-separated list: non-empty, no leading/trailing/double commas (which would
* produce an empty item), and every item passing @a is_valid_item_char. */
bool validate_csv_list(const std::string& value, bool (*is_valid_item_char)(char)) {
if (value.empty()) {
return false;
}
size_t start = 0;
while (true) {
auto comma = value.find(',', start);
auto end = comma == std::string::npos ? value.size() : comma;
if (end == start || !validate_string(value.substr(start, end - start), is_valid_item_char)) {
return false;
}
if (comma == std::string::npos) {
return true;
}
start = comma + 1;
}
}
/** manifest.properties format: "key=value" lines, "[section]" lines prefix every following key
* until the next section, "#" lines are comments, blank lines are skipped. Deliberately a local,
* minimal re-implementation rather than depending on Tactility's file::loadPropertiesFile() -
@ -130,6 +150,18 @@ bool app_metadata_is_valid_version_code(const std::string& version) {
});
}
bool app_metadata_is_valid_device_arch_list(const std::string& value) {
return validate_csv_list(value, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0;
});
}
bool app_metadata_is_valid_device_id_list(const std::string& value) {
return validate_csv_list(value, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '-';
});
}
bool app_metadata_copy_bounded(char* dest, size_t dest_size, const std::string& value) {
if (value.size() >= dest_size) {
return false;
@ -141,6 +173,10 @@ bool app_metadata_copy_bounded(char* dest, size_t dest_size, const std::string&
error_t app_metadata_parse(const char* path, struct AppMetadata* out_metadata) {
LOG_I(TAG, "Parsing manifest %s", path);
// Both requires.* fields are optional in V2 and unwritten by V1; zeroing here (rather than
// relying on the caller) guarantees they read back as empty ("unrestricted") either way.
*out_metadata = {};
std::map<std::string, std::string> properties;
std::string first_line;
if (!load_properties(path, properties, first_line)) {

View File

@ -99,5 +99,33 @@ bool app_metadata_parse_v2(const std::map<std::string, std::string>& properties,
return false;
}
// requires (optional; if present, must be a non-empty comma-separated list)
auto device_arch_iterator = properties.find("requires.device.arch");
if (device_arch_iterator != properties.end()) {
const std::string& device_arch = device_arch_iterator->second;
if (!app_metadata_is_valid_device_arch_list(device_arch)) {
LOG_E(TAG, "Invalid requires.device.arch");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.requires_device_arch, sizeof(out_metadata.requires_device_arch), device_arch)) {
LOG_E(TAG, "requires.device.arch too long");
return false;
}
}
auto device_id_iterator = properties.find("requires.device.id");
if (device_id_iterator != properties.end()) {
const std::string& device_id = device_id_iterator->second;
if (!app_metadata_is_valid_device_id_list(device_id)) {
LOG_E(TAG, "Invalid requires.device.id");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.requires_device_id, sizeof(out_metadata.requires_device_id), device_id)) {
LOG_E(TAG, "requires.device.id too long");
return false;
}
}
return true;
}