diff --git a/Modules/app-module/include/app/metadata.h b/Modules/app-module/include/app/metadata.h index 6cd039ec8..55b1f0819 100644 --- a/Modules/app-module/include/app/metadata.h +++ b/Modules/app-module/include/app/metadata.h @@ -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]; }; /** diff --git a/Modules/app-module/private/app/private/app_metadata_parsing_internal.h b/Modules/app-module/private/app/private/app_metadata_parsing_internal.h index f611b0a50..1279ab9a4 100644 --- a/Modules/app-module/private/app/private/app_metadata_parsing_internal.h +++ b/Modules/app-module/private/app/private/app_metadata_parsing_internal.h @@ -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/ 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 */ diff --git a/Modules/app-module/source/app_metadata_parsing.cpp b/Modules/app-module/source/app_metadata_parsing.cpp index 208f7ea99..6da82a3ed 100644 --- a/Modules/app-module/source/app_metadata_parsing.cpp +++ b/Modules/app-module/source/app_metadata_parsing.cpp @@ -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(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(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 properties; std::string first_line; if (!load_properties(path, properties, first_line)) { diff --git a/Modules/app-module/source/app_metadata_parsing_v2.cpp b/Modules/app-module/source/app_metadata_parsing_v2.cpp index fe7a58991..9a08003b6 100644 --- a/Modules/app-module/source/app_metadata_parsing_v2.cpp +++ b/Modules/app-module/source/app_metadata_parsing_v2.cpp @@ -99,5 +99,33 @@ bool app_metadata_parse_v2(const std::map& 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; }