Implemented multi-binary app packaging (#648)

This commit is contained in:
Ken Van Hoeylandt
2026-09-09 20:05:04 +02:00
committed by GitHub
parent 2496dea5c2
commit 8556103eb1
62 changed files with 1746 additions and 981 deletions
+93 -75
View File
@@ -2,10 +2,14 @@
#include <app/install.h>
#include <app/manager.h>
#include <app/metadata.h>
#include <app/package_manifest.h>
#include <app/private/binary_path.h>
#include <app/private/fs.h>
#include <app/private/ledger.h>
#include <app/private/package_manifest_parsing.h>
#include <TactilityCpp/Allocator.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/log.h>
@@ -29,7 +33,7 @@ constexpr auto* TAG = "app_install";
namespace {
// region Filesystem helpers (app-module may not depend upward on Tactility::file - see
// region Filesystem helpers (app-module may not depend upward on Tactility::file; see
// app_metadata_parsing.cpp for the same constraint applied to properties-file loading)
std::string last_path_segment(const std::string& path) {
@@ -164,9 +168,9 @@ bool untar(const std::string& tar_path, const std::string& destination_path) {
// endregion
// region Staging-path lock: at most one caller may clean up/populate a given staging_path at a
// time, keyed by source basename. The HTTP server and app tasks can call app_install()
// concurrently, e.g. two uploads sharing a source basename - without this, one call's cleanup
// can delete or overwrite the staging directory another call is still extracting into.
// time, keyed by source basename. The HTTP server and app tasks can call app_install_package()
// concurrently, e.g. two uploads sharing a source basename; without this lock, one call's
// cleanup could delete or overwrite the staging directory another call is still extracting into.
struct StagingLock {
Mutex mutex {};
@@ -223,18 +227,19 @@ void release_staging_lock(const std::string& path) {
// endregion
// region Installed-app registry: owns the AppManifest (and its id/name/path strings) that
// app_manager's ledger only keeps a non-owning pointer to (see app_manager_add()'s contract).
// region Installed-package registry: owns the AppManifests (and their backing location-path
// strings) that app_manager's ledger only keeps non-owning pointers to (see app_manager_add()'s
// contract). AppManifest::id/name own their own storage directly (fixed arrays), so this doesn't
// need to separately own those.
struct InstalledAppRecord {
std::string id;
std::string name;
std::string path;
AppManifest manifest {};
struct InstalledPackageRecord {
std::string path; // install directory
std::vector<AppManifest> manifests; // one per AppManifest the package declared
std::vector<std::string> locations; // backs manifests[i].location.location, same indices
};
struct InstallRegistry {
std::unordered_map<std::string, std::unique_ptr<InstalledAppRecord>> apps;
std::unordered_map<std::string, std::unique_ptr<InstalledPackageRecord>> apps;
Mutex mutex {};
InstallRegistry() { mutex_construct(&mutex); }
@@ -245,46 +250,68 @@ InstallRegistry& install_registry() {
return registry;
}
// Registers @a app_dir_path (already confirmed to hold a valid manifest.properties, parsed into
// @a metadata) with app_manager_add(), taking ownership of its id/name/path strings.
// @warning Caller must hold install_registry().mutex, and must have already ensured
// @a metadata.app_id isn't already registered (app_manager_add() rejects duplicates, but the
// InstalledAppRecord for the earlier registration would leak since this always inserts fresh).
error_t register_installed_app_locked(const std::string& app_dir_path, const AppMetadata& metadata) {
// Registers every AppManifest in @a bindings (@a count of them) with app_manager_add(), taking
// ownership of their backing location strings, then registers @a package itself.
// @warning Caller must hold install_registry().mutex.
error_t register_installed_package_locked(const PackageManifest& package, const std::string& install_path, const AppManifestBinding* bindings, size_t count) {
auto& registry = install_registry();
auto record = std::make_unique<InstalledAppRecord>();
record->id = metadata.app_id;
record->name = metadata.app_name;
record->path = app_dir_path;
record->manifest = AppManifest {
.id = record->id.c_str(),
.name = record->name.c_str(),
.category = APP_CATEGORY_USER,
.location = { APP_LOCATION_PATH, const_cast<char*>(record->path.c_str()) },
.flags = 0,
.stack = { .depth = static_cast<uint16_t>(metadata.stack_depth), .desired_memory_capability = 0 },
};
auto record = std::make_unique<InstalledPackageRecord>();
record->path = install_path;
// Sized once, up front: manifests[i].location.location points into locations[i].c_str(),
// which would dangle if either vector reallocated afterward.
record->manifests.resize(count);
record->locations.resize(count);
// Belt-and-braces: app_install()'s earlier app_manager_remove() call is meant to have
// already cleared any stale registration for this id (e.g. left over from
// app_manager_install_path_scan()'s separate registry), but that call happens before the
// tarball is even extracted - remove once more, right before add, so a duplicate id can
// never turn a filesystem-level install success into a reported failure.
app_manager_remove(record->id.c_str());
error_t add_result = app_manager_add(&record->manifest);
if (add_result != ERROR_NONE) {
LOG_E(TAG, "Failed to register app '%s': %s", record->id.c_str(), error_to_string(add_result));
return add_result;
for (size_t i = 0; i < count; i++) {
record->manifests[i] = bindings[i].manifest;
record->locations[i] = app_resolve_binary_path(install_path, bindings[i].binary);
record->manifests[i].location = { APP_LOCATION_PATH, const_cast<char*>(record->locations[i].c_str()) };
}
registry.apps[record->id] = std::move(record);
for (size_t i = 0; i < count; i++) {
// The caller's earlier uninstall_locked() call is meant to have already cleared any stale registration for this id
// (e.g. left over from app_manager_install_path_scan()'s separate registry),
// but that call happens before the package is even extracted / the binaries moved into place; remove once more
// right before add, so a duplicate id can never turn a filesystem-level install success into a reported failure.
// Only if installed (APP_LOCATION_PATH) - never steal a built-in's id.
AppManifest existing {};
if (app_manager_find_manifest(record->manifests[i].id, &existing) == ERROR_NONE && existing.location.type == APP_LOCATION_PATH) {
app_manager_remove(record->manifests[i].id);
}
error_t add_result = app_manager_add(&record->manifests[i]);
if (add_result != ERROR_NONE) {
LOG_E(TAG, "Failed to register app '%s': %s", record->manifests[i].id, error_to_string(add_result));
// All-or-nothing: unregister whatever this package already added before failing.
for (size_t j = 0; j < i; j++) {
app_manager_remove(record->manifests[j].id);
}
return add_result;
}
}
std::vector<const char*> app_id_ptrs;
app_id_ptrs.reserve(count);
for (size_t i = 0; i < count; i++) {
app_id_ptrs.push_back(record->manifests[i].id);
}
app_manager_remove_package(package.id);
error_t add_package_result = app_manager_add_package(&package, app_id_ptrs.data(), app_id_ptrs.size());
if (add_package_result != ERROR_NONE) {
LOG_E(TAG, "Failed to register package '%s': %s", package.id, error_to_string(add_package_result));
for (size_t i = 0; i < count; i++) {
app_manager_remove(record->manifests[i].id);
}
return add_package_result;
}
registry.apps[package.id] = std::move(record);
return ERROR_NONE;
}
// Stops every currently-running instance of @a manifest. Collects matching instance ids while
// holding the ledger lock, then calls app_manager_stop() on each after releasing it - that call
// holding the ledger lock, then calls app_manager_stop() on each after releasing it: that call
// bound-joins the instance's thread, which must not happen while the ledger mutex (also taken by
// the instance's own thread_main()) is held, or the two threads would deadlock each other.
void stop_all_instances_of(const AppManifest* manifest) {
@@ -305,20 +332,22 @@ void stop_all_instances_of(const AppManifest* manifest) {
}
// Caller must already hold install_registry().mutex
error_t uninstall_locked(const std::string& app_id) {
error_t uninstall_locked(const std::string& package_id) {
auto& registry = install_registry();
auto iterator = registry.apps.find(app_id);
auto iterator = registry.apps.find(package_id);
if (iterator == registry.apps.end()) {
return ERROR_NOT_FOUND;
}
// Can't uninstall in-memory apps
if (iterator->second->manifest.location.type != APP_LOCATION_PATH) {
return ERROR_NOT_SUPPORTED;
for (const auto& manifest : iterator->second->manifests) {
// Can't uninstall in-memory apps
if (manifest.location.type != APP_LOCATION_PATH) {
continue;
}
stop_all_instances_of(&manifest);
app_manager_remove(manifest.id);
}
stop_all_instances_of(&iterator->second->manifest);
app_manager_remove(app_id.c_str());
app_manager_remove_package(package_id.c_str());
delete_recursively(iterator->second->path);
registry.apps.erase(iterator);
@@ -352,7 +381,7 @@ error_t app_get_install_path(const char* app_id, char* path, size_t path_size) {
}
error_t app_install(const char* source_path) {
LOG_I(TAG, "Installing app from %s", source_path);
LOG_I(TAG, "Installing app package from %s", source_path);
std::string app_parent_path;
if (!get_app_install_directory(app_parent_path)) {
@@ -389,8 +418,9 @@ error_t app_install(const char* source_path) {
return ERROR_INVALID_ARGUMENT;
}
AppMetadata metadata {};
if (app_metadata_parse(manifest_path.c_str(), &metadata) != ERROR_NONE) {
PackageManifest package {};
std::vector<AppManifestBinding, tt::OptExternalAllocator<AppManifestBinding>> app_bindings;
if (app_package_manifest_parse_into(manifest_path.c_str(), package, app_bindings) != ERROR_NONE) {
LOG_E(TAG, "Install failed: invalid manifest");
delete_recursively(staging_path);
release_staging_lock(staging_path);
@@ -400,24 +430,13 @@ error_t app_install(const char* source_path) {
auto& registry = install_registry();
mutex_lock(&registry.mutex);
// Replace any previous install of this app id (mirrors the old install()'s "already
// running/present" handling). uninstall_locked() only clears app_install.cpp's own
// registry - the same app id may instead be registered by app_manager_install_path_scan()
// (manager.cpp's separate registry, scanning this same directory tree), which
// uninstall_locked() doesn't know about. Clear the app-manager registration unconditionally
// too, or app_manager_add() below rejects the re-add as a duplicate.
uninstall_locked(metadata.app_id);
// Replace any previous installation of this package - this also handles the app_manager
// registrations of its old AppManifests, so there's no separate app_manager_remove() needed
// here (register_installed_package_locked() below still defends against a stale registration
// per individual id, e.g. one left by app_manager_install_path_scan()'s separate registry).
uninstall_locked(package.id);
error_t remove_result = app_manager_remove(metadata.app_id);
if (remove_result != ERROR_NONE && remove_result != ERROR_NOT_FOUND) {
LOG_E(TAG, "Install failed: failed to remove existing installation");
mutex_unlock(&registry.mutex);
delete_recursively(staging_path);
release_staging_lock(staging_path);
return ERROR_RESOURCE;
}
auto final_path = app_parent_path + "/" + metadata.app_id;
auto final_path = app_parent_path + "/" + package.id;
delete_recursively(final_path);
if (rename(staging_path.c_str(), final_path.c_str()) != 0) {
@@ -429,9 +448,8 @@ error_t app_install(const char* source_path) {
}
release_staging_lock(staging_path);
// Only remaining failure mode is a duplicate id - can't happen, uninstall_locked() above
// already removed any previous registration for this exact id.
error_t add_result = register_installed_app_locked(final_path, metadata);
// app_bindings.size(), not package.app_manifest_count - the safe bound to index by.
error_t add_result = register_installed_package_locked(package, final_path, app_bindings.data(), app_bindings.size());
mutex_unlock(&registry.mutex);
return add_result;
+174 -44
View File
@@ -1,13 +1,17 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/manager.h>
#include <app/metadata.h>
#include <app/package_manifest.h>
#include <app/private/arguments.h>
#include <app/private/binary_path.h>
#include <app/private/fd_table.h>
#include <app/private/fs.h>
#include <app/private/ledger.h>
#include <app/private/manager_internal.h>
#include <app/private/package_manifest_parsing.h>
#include <app/private/scheduler.h>
#include <TactilityCpp/Allocator.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/error.h>
#include <tactility/log.h>
@@ -72,6 +76,67 @@ void app_manager_for_each_manifest(AppManifestVisitorFn visitor, void* context)
mutex_unlock(&ledger.mutex);
}
error_t app_manager_add_package(const PackageManifest* package, const char* const* app_ids, size_t app_id_count) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
if (ledger.packages.contains(package->id)) {
mutex_unlock(&ledger.mutex);
LOG_E(TAG, "Package with id '%s' is already registered", package->id);
return ERROR_INVALID_ARGUMENT;
}
AppPackageRecord record { .package = *package };
record.app_ids.reserve(app_id_count);
for (size_t i = 0; i < app_id_count; i++) {
record.app_ids.emplace_back(app_ids[i]);
}
ledger.packages[package->id] = std::move(record);
mutex_unlock(&ledger.mutex);
return ERROR_NONE;
}
error_t app_manager_remove_package(const char* package_id) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
auto iterator = ledger.packages.find(package_id);
if (iterator == ledger.packages.end()) {
mutex_unlock(&ledger.mutex);
return ERROR_NOT_FOUND;
}
ledger.packages.erase(iterator);
mutex_unlock(&ledger.mutex);
return ERROR_NONE;
}
error_t app_manager_find_package(const char* package_id, PackageManifest* out_package) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
auto iterator = ledger.packages.find(package_id);
if (iterator == ledger.packages.end()) {
mutex_unlock(&ledger.mutex);
return ERROR_NOT_FOUND;
}
*out_package = iterator->second.package;
mutex_unlock(&ledger.mutex);
return ERROR_NONE;
}
void app_manager_for_each_package(AppPackageVisitorFn visitor, void* context) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
for (auto& [id, record] : ledger.packages) {
std::vector<const char*> app_id_ptrs;
app_id_ptrs.reserve(record.app_ids.size());
for (const auto& app_id : record.app_ids) {
app_id_ptrs.push_back(app_id.c_str());
}
AppPackage pkg { .package = record.package, .app_id_count = app_id_ptrs.size(), .app_ids = app_id_ptrs.data() };
visitor(&pkg, context);
}
mutex_unlock(&ledger.mutex);
}
error_t app_manager_start_internal(const AppManifest* manifest, AppLocation location, AppStackConfig stack, AppInstanceId parent_instance_id, int argc, const char* const argv_in[], const AppStreamBinding* bindings, size_t binding_count, AppInstanceId* out_app_instance_id) {
char** argv = app_arguments_copy(argc, argv_in);
if (argc > 0 && argv == nullptr) {
@@ -200,19 +265,19 @@ error_t app_manager_get_topmost_app_id(char* buffer, size_t buffer_size) {
namespace {
// Owns the AppManifest (and its id/name/path strings) that app_manager_add() only keeps a
// non-owning pointer to. Separate from app_install.cpp's registry: scanning only
// adds/removes registrations, never touches disk or running instances.
struct ScannedAppManifest {
std::string id;
std::string name;
std::string path;
AppManifest manifest {};
// Owns the AppManifests (and their backing location-path strings) that app_manager_add() only
// keeps non-owning pointers to. Separate from app_install.cpp's registry: scanning only
// adds/removes registrations, never touches disk or running instances. AppManifest::id/name own
// their own storage directly (fixed arrays), so this doesn't need to separately own those.
struct ScannedPackageManifest {
std::string path; // scanned directory
std::vector<AppManifest> manifests; // one per AppManifest the package declared
std::vector<std::string> locations; // backs manifests[i].location.location, same indices
};
struct InstallPathRegistry {
std::vector<std::string> paths;
std::unordered_map<std::string, std::unique_ptr<ScannedAppManifest>> scanned;
std::unordered_map<std::string, std::unique_ptr<ScannedPackageManifest>> scanned;
Mutex mutex {};
InstallPathRegistry() { mutex_construct(&mutex); }
@@ -249,50 +314,69 @@ void app_manager_install_path_scan(void) {
app_fs_list_direct_subdirectories(root, found_app_dirs);
}
// Snapshot once so the rest of the scan doesn't hold registry.mutex.
// Snapshot once so the rest of the scan doesn't hold registry.mutex. Keeps each known
// package's own manifest ids too, so a package whose directory has disappeared can have all
// of its (possibly several) app_manager registrations removed below, not just one.
struct KnownPackage {
std::string path;
std::vector<std::string> manifest_ids;
};
mutex_lock(&registry.mutex);
std::unordered_map<std::string, std::string> known_paths;
std::unordered_map<std::string, KnownPackage> known_packages;
for (const auto& [id, record] : registry.scanned) {
known_paths.emplace(id, record->path);
KnownPackage known { .path = record->path };
for (const auto& manifest : record->manifests) {
known.manifest_ids.emplace_back(manifest.id);
}
known_packages.emplace(id, std::move(known));
}
mutex_unlock(&registry.mutex);
// Parses without registry.mutex held; filesystem IO is slow.
std::vector<std::unique_ptr<ScannedAppManifest>> new_records;
std::vector<std::unique_ptr<ScannedPackageManifest>> new_records;
std::vector<std::string> new_package_ids;
std::vector<PackageManifest> new_packages;
for (const auto& app_dir : found_app_dirs) {
auto manifest_path = app_dir + "/manifest.properties";
if (!app_fs_is_file(manifest_path)) {
continue;
}
AppMetadata metadata {};
if (app_metadata_parse(manifest_path.c_str(), &metadata) != ERROR_NONE) {
PackageManifest package {};
// Heap-allocated (not a stack array - too large for a typical app task's stack; this
// function runs on whichever task calls app_manager_install_path_scan(), e.g. Boot's, via
// registerInstalledAppsFromFileSystems()) and sized to fit exactly, not pre-allocated to
// some fixed maximum (see app_package_manifest_parse_into()). OptExternalAllocator prefers
// PSRAM for this transient buffer, freeing up scarce internal RAM.
std::vector<AppManifestBinding, tt::OptExternalAllocator<AppManifestBinding>> app_bindings;
if (app_package_manifest_parse_into(manifest_path.c_str(), package, app_bindings) != ERROR_NONE) {
LOG_W(TAG, "Invalid manifest at %s", manifest_path.c_str());
continue;
}
if (known_paths.contains(metadata.app_id)) {
if (known_packages.contains(package.id)) {
continue;
}
auto record = std::make_unique<ScannedAppManifest>();
record->id = metadata.app_id;
record->name = metadata.app_name;
auto record = std::make_unique<ScannedPackageManifest>();
record->path = app_dir;
record->manifest = AppManifest {
.id = record->id.c_str(),
.name = record->name.c_str(),
.category = APP_CATEGORY_USER,
.location = { APP_LOCATION_PATH, const_cast<char*>(record->path.c_str()) },
.flags = 0,
.stack = { .depth = static_cast<uint16_t>(metadata.stack_depth), .desired_memory_capability = 0 },
};
// Sized once, up front: manifests[i].location.location points into locations[i].c_str(),
// which would dangle if either vector reallocated afterward.
record->manifests.resize(app_bindings.size());
record->locations.resize(app_bindings.size());
for (size_t i = 0; i < app_bindings.size(); i++) {
record->manifests[i] = app_bindings[i].manifest;
record->locations[i] = app_resolve_binary_path(app_dir, app_bindings[i].binary);
record->manifests[i].location = { APP_LOCATION_PATH, const_cast<char*>(record->locations[i].c_str()) };
}
new_package_ids.emplace_back(package.id);
new_packages.push_back(package);
new_records.push_back(std::move(record));
}
std::vector<std::string> missing_ids;
for (const auto& [id, path] : known_paths) {
if (!app_fs_is_directory(path)) {
for (const auto& [id, known] : known_packages) {
if (!app_fs_is_directory(known.path)) {
missing_ids.push_back(id);
}
}
@@ -301,23 +385,60 @@ void app_manager_install_path_scan(void) {
// registry.mutex would fix a lock order an opposite-order caller could deadlock against.
// registry.mutex is retaken afterward only to publish the in-memory results.
for (const auto& id : missing_ids) {
app_manager_remove(id.c_str());
}
std::vector<std::unique_ptr<ScannedAppManifest>> added_records;
for (auto& record : new_records) {
if (app_manager_add(&record->manifest) == ERROR_NONE) {
added_records.push_back(std::move(record));
} else {
LOG_E(TAG, "Failed to register app %s (duplicate id?)", record->id.c_str());
for (const auto& manifest_id : known_packages.at(id).manifest_ids) {
app_manager_remove(manifest_id.c_str());
}
app_manager_remove_package(id.c_str());
}
std::vector<std::unique_ptr<ScannedPackageManifest>> added_records;
std::vector<std::string> added_package_ids;
for (size_t r = 0; r < new_records.size(); r++) {
auto& record = new_records[r];
// known_packages is only a pre-scan snapshot - two new directories can still share an id.
if (std::ranges::find(added_package_ids, new_package_ids[r]) != added_package_ids.end()) {
LOG_W(TAG, "Skipping duplicate package id %s found in this scan", new_package_ids[r].c_str());
continue;
}
size_t added_count = 0;
for (; added_count < record->manifests.size(); added_count++) {
if (app_manager_add(&record->manifests[added_count]) != ERROR_NONE) {
LOG_E(TAG, "Failed to register app %s (duplicate id?)", record->manifests[added_count].id);
break;
}
}
if (added_count != record->manifests.size()) {
// All-or-nothing: unregister whatever this package already added before failing.
for (size_t j = 0; j < added_count; j++) {
app_manager_remove(record->manifests[j].id);
}
continue;
}
std::vector<const char*> app_id_ptrs;
app_id_ptrs.reserve(record->manifests.size());
for (const auto& app_manifest : record->manifests) {
app_id_ptrs.push_back(app_manifest.id);
}
if (app_manager_add_package(&new_packages[r], app_id_ptrs.data(), app_id_ptrs.size()) != ERROR_NONE) {
LOG_E(TAG, "Failed to register package %s (duplicate id?)", new_packages[r].id);
for (const auto& app_manifest : record->manifests) {
app_manager_remove(app_manifest.id);
}
continue;
}
added_package_ids.push_back(new_package_ids[r]);
added_records.push_back(std::move(record));
}
mutex_lock(&registry.mutex);
for (const auto& id : missing_ids) {
registry.scanned.erase(id);
}
for (auto& record : added_records) {
registry.scanned[record->id] = std::move(record);
for (size_t i = 0; i < added_records.size(); i++) {
registry.scanned[added_package_ids[i]] = std::move(added_records[i]);
}
mutex_unlock(&registry.mutex);
}
@@ -332,7 +453,10 @@ error_t app_manager_install_path_uninstall(const char* app_id) {
return ERROR_NOT_FOUND;
}
const AppManifest* manifest = &iterator->second->manifest;
// Pointer, not a copy: the ledger's own AppInstanceRecord::manifest pointers (set by
// app_manager_add() from this exact vector) are compared against it by address below, same
// as the pre-existing single-manifest version of this function did.
const std::vector<AppManifest>* manifests = &iterator->second->manifests;
auto path = iterator->second->path;
mutex_unlock(&registry.mutex);
@@ -342,8 +466,11 @@ error_t app_manager_install_path_uninstall(const char* app_id) {
auto& ledger = app_ledger();
mutex_lock(&ledger.mutex);
for (const auto& [id, record] : ledger.instances) {
if (record.manifest == manifest) {
instance_ids.push_back(id);
for (const auto& manifest : *manifests) {
if (record.manifest == &manifest) {
instance_ids.push_back(id);
break;
}
}
}
mutex_unlock(&ledger.mutex);
@@ -354,7 +481,10 @@ error_t app_manager_install_path_uninstall(const char* app_id) {
// app_manager_remove() takes ledger.mutex; call outside registry.mutex too, matching
// the lock order in app_manager_install_path_scan().
app_manager_remove(app_id);
for (const auto& manifest : *manifests) {
app_manager_remove(manifest.id);
}
app_manager_remove_package(app_id);
// Delete before erasing the scan record, so a failed deletion still leaves the
// entry discoverable for a retry.
+18 -3
View File
@@ -1,15 +1,30 @@
#include <app/manifest.h>
#include <app/private/metadata_parsing_internal.h>
#include <app/private/package_manifest_parsing.h>
#include <stdlib.h>
#include <string.h>
extern "C" {
bool app_id_is_valid(const char* id) {
bool app_manifest_id_is_valid(const char* id) {
auto size = strlen(id);
return size >= 5 && size <= APP_ID_LENGTH && app_metadata_validate_string(id, [](char c) {
return size >= 5 && size <= APP_MANIFEST_ID_LENGTH && app_package_manifest_validate_string(id, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '.';
});
}
bool app_manifest_name_is_valid(const char* name) {
auto size = strlen(name);
return size >= 2 && size <= APP_MANIFEST_NAME_LENGTH && app_package_manifest_validate_string(name, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == ' ' || c == '-';
});
}
bool app_manifest_stack_size_is_valid(const char* value) {
// 10 digits is the maximum decimal width of uint32_t.
auto size = strlen(value);
return size > 0 && size <= 10 && app_package_manifest_validate_string(value, [](char c) {
return std::isdigit(static_cast<unsigned char>(c)) != 0;
});
}
}
@@ -1,190 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/metadata.h>
#include <app/private/metadata_parsing_internal.h>
#include <tactility/log.h>
#include <cctype>
#include <cstring>
#include <fstream>
#include <map>
#include <string>
constexpr auto* TAG = "app_metadata";
bool app_metadata_validate_string(const std::string& value, bool (*is_valid_char)(char)) {
for (char c: value) {
if (!is_valid_char(c)) {
return false;
}
}
return true;
}
namespace {
#define validate_string app_metadata_validate_string
std::string trim(const std::string& value) {
constexpr auto* whitespace = " \t\r\n";
auto start = value.find_first_not_of(whitespace);
if (start == std::string::npos) {
return "";
}
auto end = value.find_last_not_of(whitespace);
return value.substr(start, end - start + 1);
}
/** 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. */
bool validate_csv_list(const std::string& value, bool (*is_valid_item)(const std::string&)) {
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 || !is_valid_item(value.substr(start, end - start))) {
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() -
* app-module (like every other kernel module) may not depend upward on the Tactility layer. */
bool load_properties(const std::string& path, std::map<std::string, std::string>& out_properties, std::string& out_first_line) {
std::ifstream file(path);
if (!file.is_open()) {
return false;
}
std::string line;
std::string section_prefix;
bool got_first_line = false;
while (std::getline(file, line)) {
auto trimmed_line = trim(line);
if (trimmed_line.empty() || trimmed_line.starts_with("#")) {
continue;
}
if (!got_first_line) {
out_first_line = trimmed_line;
got_first_line = true;
}
if (trimmed_line.starts_with("[")) {
section_prefix = trimmed_line;
continue;
}
auto separator_index = trimmed_line.find('=');
if (separator_index == std::string::npos) {
LOG_E(TAG, "Failed to parse manifest line (skipped): %s", trimmed_line.c_str());
continue;
}
auto key = section_prefix + trim(trimmed_line.substr(0, separator_index));
auto value = trim(trimmed_line.substr(separator_index + 1));
out_properties[key] = value;
}
return true;
}
} // namespace
bool app_metadata_get_value(const std::map<std::string, std::string>& properties, const std::string& key, std::string& out_value) {
const auto iterator = properties.find(key);
if (iterator == properties.end()) {
LOG_E(TAG, "Failed to find %s in manifest", key.c_str());
return false;
}
out_value = iterator->second;
return true;
}
bool app_metadata_is_valid_format_version(const std::string& version) {
return !version.empty() && validate_string(version, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '.';
});
}
bool app_metadata_is_valid_name(const std::string& name) {
return name.size() >= 2 && name.size() <= APP_METADATA_APP_NAME_LENGTH && validate_string(name, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == ' ' || c == '-';
});
}
bool app_metadata_is_valid_version_name(const std::string& version) {
return !version.empty() && version.size() <= APP_METADATA_APP_VERSION_NAME_LENGTH && validate_string(version, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '.' || c == '-' || c == '_';
});
}
bool app_metadata_is_valid_version_code(const std::string& version) {
// 20 digits is the maximum decimal width of uint64_t.
return !version.empty() && version.size() <= 20 && validate_string(version, [](char c) {
return std::isdigit(static_cast<unsigned char>(c)) != 0;
});
}
bool app_metadata_is_valid_stack_size(const std::string& value) {
// 10 digits is the maximum decimal width of uint32_t.
return !value.empty() && value.size() <= 10 && validate_string(value, [](char c) {
return std::isdigit(static_cast<unsigned char>(c)) != 0;
});
}
bool app_metadata_is_valid_device_id_list(const std::string& value) {
return validate_csv_list(value, [](const std::string& item) {
bool has_alnum = false;
for (char c: item) {
if (std::isalnum(static_cast<unsigned char>(c)) != 0) {
has_alnum = true;
} else if (c != '-') {
return false;
}
}
return has_alnum;
});
}
bool app_metadata_copy_bounded(char* dest, size_t dest_size, const std::string& value) {
if (value.size() >= dest_size) {
return false;
}
memcpy(dest, value.c_str(), value.size() + 1);
return true;
}
error_t app_metadata_parse(const char* path, struct AppMetadata* out_metadata) {
LOG_I(TAG, "Parsing manifest %s", path);
// requires_device_id is optional in V2 and unwritten by V1; zeroing here (rather than relying
// on the caller) guarantees it reads 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)) {
LOG_E(TAG, "Failed to load manifest at %s", path);
return ERROR_NOT_FOUND;
}
// The V1 format's first line is always the literal "[manifest]" section header; V2 files are
// flat from the first line onward.
bool is_v1_format = first_line == "[manifest]";
bool success = is_v1_format
? app_metadata_parse_v1(properties, *out_metadata)
: app_metadata_parse_v2(properties, *out_metadata);
return success ? ERROR_NONE : ERROR_INVALID_ARGUMENT;
}
@@ -1,107 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
#include "app/manifest.h"
#include <app/metadata.h>
#include <app/private/metadata_parsing_internal.h>
#include <charconv>
#include <tactility/log.h>
constexpr auto* TAG = "app_metadata_v1";
bool app_metadata_parse_v1(const std::map<std::string, std::string>& properties, AppMetadata& out_metadata) {
// [manifest]
LOG_W(TAG, "This manifest version is deprecated. Replace it with the newer version.");
std::string format_version;
if (!app_metadata_get_value(properties, "[manifest]version", format_version)) {
return false;
}
if (!app_metadata_is_valid_format_version(format_version)) {
LOG_E(TAG, "Invalid version");
return false;
}
// [app]
std::string id;
if (!app_metadata_get_value(properties, "[app]id", id)) {
return false;
}
if (!app_id_is_valid(id.c_str())) {
LOG_E(TAG, "Invalid app id");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_id, sizeof(out_metadata.app_id), id)) {
LOG_E(TAG, "App id too long");
return false;
}
std::string name;
if (!app_metadata_get_value(properties, "[app]name", name)) {
return false;
}
if (!app_metadata_is_valid_name(name)) {
LOG_E(TAG, "Invalid app name");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_name, sizeof(out_metadata.app_name), name)) {
LOG_E(TAG, "App name too long");
return false;
}
std::string version_name;
if (!app_metadata_get_value(properties, "[app]versionName", version_name)) {
return false;
}
if (!app_metadata_is_valid_version_name(version_name)) {
LOG_E(TAG, "Invalid app version name");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_version_name, sizeof(out_metadata.app_version_name), version_name)) {
LOG_E(TAG, "App version name too long");
return false;
}
std::string version_code_string;
if (!app_metadata_get_value(properties, "[app]versionCode", version_code_string)) {
return false;
}
if (!app_metadata_is_valid_version_code(version_code_string)) {
LOG_E(TAG, "Invalid app version code");
return false;
}
uint64_t version_code = 0;
const auto* first = version_code_string.data();
const auto* last = first + version_code_string.size();
if (std::from_chars(first, last, version_code).ec != std::errc {}) {
LOG_E(TAG, "App version code out of range");
return false;
}
out_metadata.app_version_code = version_code; // [target]
std::string target_sdk;
if (!app_metadata_get_value(properties, "[target]sdk", target_sdk)) {
return false;
}
if (!app_metadata_copy_bounded(out_metadata.target_sdk, sizeof(out_metadata.target_sdk), target_sdk)) {
LOG_E(TAG, "Target sdk too long");
return false;
}
out_metadata.stack_depth = 0;
return true;
}
@@ -1,149 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
#include "app/manifest.h"
#include <app/metadata.h>
#include <app/private/metadata_parsing_internal.h>
#include <charconv>
#include <tactility/log.h>
constexpr auto* TAG = "app_metadata_v2";
bool app_metadata_parse_v2(const std::map<std::string, std::string>& properties, AppMetadata& out_metadata) {
// manifest
std::string format_version;
if (!app_metadata_get_value(properties, "manifest.version", format_version)) {
return false;
}
if (!app_metadata_is_valid_format_version(format_version)) {
LOG_E(TAG, "Invalid version");
return false;
}
// app
std::string id;
if (!app_metadata_get_value(properties, "app.id", id)) {
return false;
}
if (!app_id_is_valid(id.c_str())) {
LOG_E(TAG, "Invalid app id");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_id, sizeof(out_metadata.app_id), id)) {
LOG_E(TAG, "App id too long");
return false;
}
std::string name;
if (!app_metadata_get_value(properties, "app.name", name)) {
return false;
}
if (!app_metadata_is_valid_name(name)) {
LOG_E(TAG, "Invalid app name");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_name, sizeof(out_metadata.app_name), name)) {
LOG_E(TAG, "App name too long");
return false;
}
std::string version_name;
if (!app_metadata_get_value(properties, "app.version.name", version_name)) {
return false;
}
if (!app_metadata_is_valid_version_name(version_name)) {
LOG_E(TAG, "Invalid app version name");
return false;
}
if (!app_metadata_copy_bounded(out_metadata.app_version_name, sizeof(out_metadata.app_version_name), version_name)) {
LOG_E(TAG, "App version name too long");
return false;
}
std::string version_code_string;
if (!app_metadata_get_value(properties, "app.version.code", version_code_string)) {
return false;
}
if (!app_metadata_is_valid_version_code(version_code_string)) {
LOG_E(TAG, "Invalid app version code");
return false;
}
uint64_t version_code = 0;
const auto* first = version_code_string.data();
const auto* last = first + version_code_string.size();
if (std::from_chars(first, last, version_code).ec != std::errc {}) {
LOG_E(TAG, "App version code out of range");
return false;
}
out_metadata.app_version_code = version_code; // [target]
// target
std::string target_sdk;
if (!app_metadata_get_value(properties, "target.sdk", target_sdk)) {
return false;
}
if (!app_metadata_copy_bounded(out_metadata.target_sdk, sizeof(out_metadata.target_sdk), target_sdk)) {
LOG_E(TAG, "Target sdk too long");
return false;
}
// requires.device.id (optional; if present, must be a non-empty comma-separated list)
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;
}
}
// app.stack.depth (optional; if present, must be a valid unsigned decimal fitting uint32_t)
auto stack_size_iterator = properties.find("app.stack.depth");
if (stack_size_iterator != properties.end()) {
const std::string& stack_size_string = stack_size_iterator->second;
if (!app_metadata_is_valid_stack_size(stack_size_string)) {
LOG_E(TAG, "Invalid app.stack.depth");
return false;
}
uint32_t stack_size = 0;
const auto* stack_size_first = stack_size_string.data();
const auto* stack_size_last = stack_size_first + stack_size_string.size();
if (std::from_chars(stack_size_first, stack_size_last, stack_size).ec != std::errc {}) {
LOG_E(TAG, "App stack depth out of range");
return false;
}
// Reject outright rather than truncating/clamping into AppStackConfig::depth (uint16_t) -
// a value like 1073741825 would otherwise silently narrow to 1, handing the app a
// catastrophically undersized stack instead of the huge one it declared.
if (stack_size > APP_STACK_SIZE_MAX) {
LOG_E(TAG, "App stack depth %u exceeds APP_STACK_SIZE_MAX(%u)", stack_size, APP_STACK_SIZE_MAX);
return false;
}
out_metadata.stack_depth = stack_size;
}
return true;
}
+15 -9
View File
@@ -5,7 +5,7 @@
#include <app/io.h>
#include <app/manager.h>
#include <app/manifest.h>
#include <app/metadata.h>
#include <app/package_manifest.h>
#include <app/paths.h>
#include <app/scheduler.h>
#include <app/start.h>
@@ -47,20 +47,21 @@ static const ModuleSymbol SYMBOLS[] = {
DEFINE_MODULE_SYMBOL(app_manager_for_each_manifest),
DEFINE_MODULE_SYMBOL(app_manager_add),
DEFINE_MODULE_SYMBOL(app_manager_remove),
DEFINE_MODULE_SYMBOL(app_manager_add_package),
DEFINE_MODULE_SYMBOL(app_manager_remove_package),
DEFINE_MODULE_SYMBOL(app_manager_find_package),
DEFINE_MODULE_SYMBOL(app_manager_for_each_package),
DEFINE_MODULE_SYMBOL(app_manager_get_topmost_instance_id),
DEFINE_MODULE_SYMBOL(app_manager_get_topmost_app_id),
DEFINE_MODULE_SYMBOL(app_manager_install_path_add),
DEFINE_MODULE_SYMBOL(app_manager_install_path_scan),
DEFINE_MODULE_SYMBOL(app_manager_install_path_uninstall),
// app/start
DEFINE_MODULE_SYMBOL(app_start),
DEFINE_MODULE_SYMBOL(app_start_for_result),
DEFINE_MODULE_SYMBOL(app_start_with_streams),
DEFINE_MODULE_SYMBOL(app_start_for_result_with_streams),
// app/manifest
DEFINE_MODULE_SYMBOL(app_id_is_valid),
// app/metadata
DEFINE_MODULE_SYMBOL(app_metadata_parse),
DEFINE_MODULE_SYMBOL(app_manifest_id_is_valid),
DEFINE_MODULE_SYMBOL(app_manifest_name_is_valid),
DEFINE_MODULE_SYMBOL(app_manifest_stack_size_is_valid),
// app/package_manifest
DEFINE_MODULE_SYMBOL(app_package_manifest_parse),
// app/paths
DEFINE_MODULE_SYMBOL(app_paths_get_user_data_directory),
DEFINE_MODULE_SYMBOL(app_paths_get_user_data_path),
@@ -68,6 +69,11 @@ static const ModuleSymbol SYMBOLS[] = {
DEFINE_MODULE_SYMBOL(app_paths_get_assets_path),
// app/scheduler
DEFINE_MODULE_SYMBOL(app_scheduler_current_app_id),
// app/start
DEFINE_MODULE_SYMBOL(app_start),
DEFINE_MODULE_SYMBOL(app_start_for_result),
DEFINE_MODULE_SYMBOL(app_start_with_streams),
DEFINE_MODULE_SYMBOL(app_start_for_result_with_streams),
// app/stream
DEFINE_MODULE_SYMBOL(app_stream_subscribe),
DEFINE_MODULE_SYMBOL(app_stream_unsubscribe),
@@ -0,0 +1,201 @@
// SPDX-License-Identifier: Apache-2.0
#include <app/manifest.h>
#include <app/package_manifest.h>
#include <app/private/package_manifest_parsing.h>
#include <tactility/log.h>
#include <cctype>
#include <cstring>
#include <fstream>
#include <map>
#include <string>
constexpr auto* TAG = "app_metadata";
bool app_package_manifest_validate_string(const std::string& value, bool (*is_valid_char)(char)) {
for (char c: value) {
if (!is_valid_char(c)) {
return false;
}
}
return true;
}
namespace {
#define validate_string app_package_manifest_validate_string
std::string trim(const std::string& value) {
constexpr auto* whitespace = " \t\r\n";
auto start = value.find_first_not_of(whitespace);
if (start == std::string::npos) {
return "";
}
auto end = value.find_last_not_of(whitespace);
return value.substr(start, end - start + 1);
}
/** 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. */
bool validate_csv_list(const std::string& value, bool (*is_valid_item)(const std::string&)) {
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 || !is_valid_item(value.substr(start, end - start))) {
return false;
}
if (comma == std::string::npos) {
return true;
}
start = comma + 1;
}
}
/** manifest.properties format: flat "key=value" lines, "#" lines are comments, blank lines are
* skipped. Deliberately a local, minimal re-implementation rather than depending on Tactility's
* file::loadPropertiesFile() - app-module (like every other kernel module) may not depend upward
* on the Tactility layer. */
bool load_properties(const std::string& path, std::map<std::string, std::string>& out_properties) {
std::ifstream file(path);
if (!file.is_open()) {
return false;
}
std::string line;
while (std::getline(file, line)) {
auto trimmed_line = trim(line);
if (trimmed_line.empty() || trimmed_line.starts_with("#")) {
continue;
}
auto separator_index = trimmed_line.find('=');
if (separator_index == std::string::npos) {
LOG_E(TAG, "Failed to parse manifest line (skipped): %s", trimmed_line.c_str());
continue;
}
auto key = trim(trimmed_line.substr(0, separator_index));
auto value = trim(trimmed_line.substr(separator_index + 1));
out_properties[key] = value;
}
return true;
}
} // namespace
bool app_package_manifest_get_value(const std::map<std::string, std::string>& properties, const std::string& key, std::string& out_value) {
const auto iterator = properties.find(key);
if (iterator == properties.end()) {
LOG_E(TAG, "Failed to find %s in manifest", key.c_str());
return false;
}
out_value = iterator->second;
return true;
}
bool app_package_manifest_is_valid_format_version(const std::string& version) {
return !version.empty() && validate_string(version, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '.';
});
}
bool app_package_manifest_is_valid_version_name(const std::string& version) {
return !version.empty() && version.size() <= PACKAGE_MANIFEST_VERSION_NAME_LENGTH && validate_string(version, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '.' || c == '-' || c == '_';
});
}
bool app_package_manifest_is_valid_version_code(const std::string& version) {
// 20 digits is the maximum decimal width of uint64_t.
return !version.empty() && version.size() <= 20 && validate_string(version, [](char c) {
return std::isdigit(static_cast<unsigned char>(c)) != 0;
});
}
bool app_package_manifest_is_valid_bool(const std::string& value) {
return value == "true" || value == "false";
}
bool app_package_manifest_is_valid_device_id_list(const std::string& value) {
return validate_csv_list(value, [](const std::string& item) {
bool has_alnum = false;
for (char c: item) {
if (std::isalnum(static_cast<unsigned char>(c)) != 0) {
has_alnum = true;
} else if (c != '-') {
return false;
}
}
return has_alnum;
});
}
bool app_package_manifest_is_valid_binary_name(const std::string& value) {
return !value.empty() && value.size() <= APP_MANIFEST_BINARY_LENGTH && validate_string(value, [](char c) {
return std::isalnum(static_cast<unsigned char>(c)) != 0 || c == '.' || c == '_' || c == '-';
});
}
bool app_package_manifest_copy_bounded(char* dest, size_t dest_size, const std::string& value) {
if (value.size() >= dest_size) {
return false;
}
memcpy(dest, value.c_str(), value.size() + 1);
return true;
}
error_t app_package_manifest_parse(const char* path, struct PackageManifest* out_package, struct AppManifestBinding* out_bindings, size_t bindings_capacity) {
LOG_I(TAG, "Parsing manifest %s", path);
// requires_device_id is optional; zeroing here (rather than relying on the caller) guarantees
// it reads back as empty ("unrestricted") when the manifest omits it.
*out_package = {};
for (size_t i = 0; i < bindings_capacity; i++) {
out_bindings[i] = {};
}
std::map<std::string, std::string> properties;
if (!load_properties(path, properties)) {
LOG_E(TAG, "Failed to load manifest at %s", path);
return ERROR_NOT_FOUND;
}
std::string format_version;
if (!app_package_manifest_get_value(properties, "manifest.version", format_version)) {
return ERROR_INVALID_ARGUMENT;
}
if (format_version == "0.2") {
return package_manifest_parse_v2(properties, *out_package, out_bindings, bindings_capacity);
} else if (format_version == "0.3") {
return package_manifest_parse_v3(properties, *out_package, out_bindings, bindings_capacity);
} else {
LOG_E(TAG, "Unsupported manifest.version: %s", format_version.c_str());
return ERROR_INVALID_ARGUMENT;
}
}
error_t app_package_manifest_parse_into(const char* path, PackageManifest& out_package, std::vector<AppManifestBinding, tt::OptExternalAllocator<AppManifestBinding>>& out_bindings) {
error_t result = app_package_manifest_parse(path, &out_package, nullptr, 0);
if (result != ERROR_NONE) {
return result;
}
out_bindings.resize(out_package.app_manifest_count);
result = app_package_manifest_parse(path, &out_package, out_bindings.data(), out_bindings.size());
if (result != ERROR_NONE) {
return result;
}
// Manifest could have changed between the two parses above.
if (out_package.app_manifest_count != out_bindings.size()) {
LOG_E(TAG, "Manifest at %s changed while being parsed", path);
return ERROR_INVALID_ARGUMENT;
}
return ERROR_NONE;
}
@@ -0,0 +1,172 @@
// SPDX-License-Identifier: Apache-2.0
#include "app/manifest.h"
#include <app/package_manifest.h>
#include <app/private/package_manifest_parsing.h>
#include <charconv>
#include <tactility/log.h>
constexpr auto* TAG = "app_metadata_v2";
error_t package_manifest_parse_v2(const std::map<std::string, std::string>& properties, PackageManifest& out_package, AppManifestBinding* out_bindings, size_t bindings_capacity) {
// manifest
std::string format_version;
if (!app_package_manifest_get_value(properties, "manifest.version", format_version)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_is_valid_format_version(format_version)) {
LOG_E(TAG, "Invalid version");
return ERROR_INVALID_ARGUMENT;
}
// app
std::string id;
if (!app_package_manifest_get_value(properties, "app.id", id)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_manifest_id_is_valid(id.c_str())) {
LOG_E(TAG, "Invalid app id");
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_package.id, sizeof(out_package.id), id)) {
LOG_E(TAG, "App id too long");
return ERROR_INVALID_ARGUMENT;
}
std::string name;
if (!app_package_manifest_get_value(properties, "app.name", name)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_manifest_name_is_valid(name.c_str())) {
LOG_E(TAG, "Invalid app name");
return ERROR_INVALID_ARGUMENT;
}
std::string version_name;
if (!app_package_manifest_get_value(properties, "app.version.name", version_name)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_is_valid_version_name(version_name)) {
LOG_E(TAG, "Invalid app version name");
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_package.version_name, sizeof(out_package.version_name), version_name)) {
LOG_E(TAG, "App version name too long");
return ERROR_INVALID_ARGUMENT;
}
std::string version_code_string;
if (!app_package_manifest_get_value(properties, "app.version.code", version_code_string)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_is_valid_version_code(version_code_string)) {
LOG_E(TAG, "Invalid app version code");
return ERROR_INVALID_ARGUMENT;
}
uint64_t version_code = 0;
const auto* first = version_code_string.data();
const auto* last = first + version_code_string.size();
if (std::from_chars(first, last, version_code).ec != std::errc {}) {
LOG_E(TAG, "App version code out of range");
return ERROR_INVALID_ARGUMENT;
}
out_package.version_code = version_code;
// target
std::string target_sdk;
if (!app_package_manifest_get_value(properties, "target.sdk", target_sdk)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_package.target_sdk, sizeof(out_package.target_sdk), target_sdk)) {
LOG_E(TAG, "Target sdk too long");
return ERROR_INVALID_ARGUMENT;
}
// requires.device.id (optional; if present, must be a non-empty comma-separated list)
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_package_manifest_is_valid_device_id_list(device_id)) {
LOG_E(TAG, "Invalid requires.device.id");
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_package.requires_device_id, sizeof(out_package.requires_device_id), device_id)) {
LOG_E(TAG, "requires.device.id too long");
return ERROR_INVALID_ARGUMENT;
}
}
// A v2 manifest always describes exactly one app, sharing the package's own id.
out_package.app_manifest_count = 1;
if (bindings_capacity == 0) {
return ERROR_NONE;
}
AppManifestBinding& binding = out_bindings[0];
AppManifest& manifest = binding.manifest;
// v2 predates the "binary" key - the single app always installs as bin/<platform>/app.{elf,so}.
if (!app_package_manifest_copy_bounded(binding.binary, sizeof(binding.binary), "app")) {
LOG_E(TAG, "Binary name too long");
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(manifest.id, sizeof(manifest.id), id)) {
LOG_E(TAG, "App id too long");
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(manifest.name, sizeof(manifest.name), name)) {
LOG_E(TAG, "App name too long");
return ERROR_INVALID_ARGUMENT;
}
// app.stack.depth (optional; if present, must be a valid unsigned decimal fitting uint32_t)
auto stack_size_iterator = properties.find("app.stack.depth");
if (stack_size_iterator != properties.end()) {
const std::string& stack_size_string = stack_size_iterator->second;
if (!app_manifest_stack_size_is_valid(stack_size_string.c_str())) {
LOG_E(TAG, "Invalid app.stack.depth");
return ERROR_INVALID_ARGUMENT;
}
uint32_t stack_size = 0;
const auto* stack_size_first = stack_size_string.data();
const auto* stack_size_last = stack_size_first + stack_size_string.size();
if (std::from_chars(stack_size_first, stack_size_last, stack_size).ec != std::errc {}) {
LOG_E(TAG, "App stack depth out of range");
return ERROR_INVALID_ARGUMENT;
}
// Reject outright rather than truncating/clamping into AppStackConfig::depth (uint16_t) -
// a value like 1073741825 would otherwise silently narrow to 1, handing the app a
// catastrophically undersized stack instead of the huge one it declared.
if (stack_size > APP_STACK_SIZE_MAX) {
LOG_E(TAG, "App stack depth %u exceeds APP_STACK_SIZE_MAX(%u)", stack_size, APP_STACK_SIZE_MAX);
return ERROR_INVALID_ARGUMENT;
}
manifest.stack.depth = static_cast<uint16_t>(stack_size);
}
// v2 predates per-app visibility - always visible.
manifest.flags = 0;
manifest.category = APP_CATEGORY_USER;
return ERROR_NONE;
}
@@ -0,0 +1,238 @@
// SPDX-License-Identifier: Apache-2.0
#include "app/manifest.h"
#include <app/package_manifest.h>
#include <app/private/package_manifest_parsing.h>
#include <charconv>
#include <format>
#include <optional>
#include <tactility/log.h>
constexpr auto* TAG = "app_metadata_v3";
namespace {
// Numeric index out of an "app.<index>.<rest>" key, or nullopt if it doesn't match that shape.
std::optional<size_t> parse_app_key_index(const std::string& key) {
constexpr auto* prefix = "app.";
constexpr size_t prefix_len = 4;
if (!key.starts_with(prefix)) {
return std::nullopt;
}
auto dot = key.find('.', prefix_len);
if (dot == std::string::npos || dot == prefix_len) {
return std::nullopt;
}
size_t index = 0;
const auto* first = key.data() + prefix_len;
const auto* last = key.data() + dot;
auto result = std::from_chars(first, last, index);
if (result.ec != std::errc {} || result.ptr != last) {
return std::nullopt;
}
return index;
}
// Parses one "app.<index>.*" block into @a out_binding.
error_t parse_app_manifest(const std::map<std::string, std::string>& properties, size_t index, AppManifestBinding& out_binding) {
auto prefix = std::format("app.{}.", index);
AppManifest& out_manifest = out_binding.manifest;
std::string id;
if (!app_package_manifest_get_value(properties, prefix + "id", id)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_manifest_id_is_valid(id.c_str())) {
LOG_E(TAG, "Invalid %sid", prefix.c_str());
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_manifest.id, sizeof(out_manifest.id), id)) {
LOG_E(TAG, "%sid too long", prefix.c_str());
return ERROR_INVALID_ARGUMENT;
}
std::string binary;
if (!app_package_manifest_get_value(properties, prefix + "binary", binary)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_is_valid_binary_name(binary)) {
LOG_E(TAG, "Invalid %sbinary", prefix.c_str());
return ERROR_INVALID_ARGUMENT;
}
// Filename (without extension) this app installs as under bin/<platform>/ - see
// app_package_manifest_parse()'s own doc.
if (!app_package_manifest_copy_bounded(out_binding.binary, sizeof(out_binding.binary), binary)) {
LOG_E(TAG, "%sbinary too long", prefix.c_str());
return ERROR_INVALID_ARGUMENT;
}
std::string name;
if (!app_package_manifest_get_value(properties, prefix + "name", name)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_manifest_name_is_valid(name.c_str())) {
LOG_E(TAG, "Invalid %sname", prefix.c_str());
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_manifest.name, sizeof(out_manifest.name), name)) {
LOG_E(TAG, "%sname too long", prefix.c_str());
return ERROR_INVALID_ARGUMENT;
}
// <index>.stack.depth (optional; if present, must be a valid unsigned decimal fitting uint32_t)
auto stack_size_iterator = properties.find(prefix + "stack.depth");
if (stack_size_iterator != properties.end()) {
const std::string& stack_size_string = stack_size_iterator->second;
if (!app_manifest_stack_size_is_valid(stack_size_string.c_str())) {
LOG_E(TAG, "Invalid %sstack.depth", prefix.c_str());
return ERROR_INVALID_ARGUMENT;
}
uint32_t stack_size = 0;
const auto* first = stack_size_string.data();
const auto* last = first + stack_size_string.size();
if (std::from_chars(first, last, stack_size).ec != std::errc {}) {
LOG_E(TAG, "%sstack.depth out of range", prefix.c_str());
return ERROR_INVALID_ARGUMENT;
}
// Reject outright rather than truncating/clamping into AppStackConfig::depth (uint16_t) -
// a value like 1073741825 would otherwise silently narrow to 1, handing the app a
// catastrophically undersized stack instead of the huge one it declared.
if (stack_size > APP_STACK_SIZE_MAX) {
LOG_E(TAG, "%sstack.depth %u exceeds APP_STACK_SIZE_MAX(%u)", prefix.c_str(), stack_size, APP_STACK_SIZE_MAX);
return ERROR_INVALID_ARGUMENT;
}
out_manifest.stack.depth = static_cast<uint16_t>(stack_size);
}
// <index>.hidden (optional; defaults to false)
auto hidden_iterator = properties.find(prefix + "hidden");
if (hidden_iterator != properties.end()) {
if (!app_package_manifest_is_valid_bool(hidden_iterator->second)) {
LOG_E(TAG, "Invalid %shidden", prefix.c_str());
return ERROR_INVALID_ARGUMENT;
}
out_manifest.flags = hidden_iterator->second == "true" ? APP_MANIFEST_FLAG_HIDDEN : 0;
}
out_manifest.category = APP_CATEGORY_USER;
return ERROR_NONE;
}
} // namespace
error_t package_manifest_parse_v3(const std::map<std::string, std::string>& properties, PackageManifest& out_package, AppManifestBinding* out_bindings, size_t bindings_capacity) {
// package-level fields: bare keys, no "app." prefix (that's reserved for the app.<index>.*
// blocks below).
std::string id;
if (!app_package_manifest_get_value(properties, "id", id)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_manifest_id_is_valid(id.c_str())) {
LOG_E(TAG, "Invalid id");
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_package.id, sizeof(out_package.id), id)) {
LOG_E(TAG, "id too long");
return ERROR_INVALID_ARGUMENT;
}
std::string version_name;
if (!app_package_manifest_get_value(properties, "version.name", version_name)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_is_valid_version_name(version_name)) {
LOG_E(TAG, "Invalid version.name");
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_package.version_name, sizeof(out_package.version_name), version_name)) {
LOG_E(TAG, "version.name too long");
return ERROR_INVALID_ARGUMENT;
}
std::string version_code_string;
if (!app_package_manifest_get_value(properties, "version.code", version_code_string)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_is_valid_version_code(version_code_string)) {
LOG_E(TAG, "Invalid version.code");
return ERROR_INVALID_ARGUMENT;
}
uint64_t version_code = 0;
const auto* first = version_code_string.data();
const auto* last = first + version_code_string.size();
if (std::from_chars(first, last, version_code).ec != std::errc {}) {
LOG_E(TAG, "version.code out of range");
return ERROR_INVALID_ARGUMENT;
}
out_package.version_code = version_code;
std::string target_sdk;
if (!app_package_manifest_get_value(properties, "target.sdk", target_sdk)) {
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_package.target_sdk, sizeof(out_package.target_sdk), target_sdk)) {
LOG_E(TAG, "target.sdk too long");
return ERROR_INVALID_ARGUMENT;
}
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_package_manifest_is_valid_device_id_list(device_id)) {
LOG_E(TAG, "Invalid requires.device.id");
return ERROR_INVALID_ARGUMENT;
}
if (!app_package_manifest_copy_bounded(out_package.requires_device_id, sizeof(out_package.requires_device_id), device_id)) {
LOG_E(TAG, "requires.device.id too long");
return ERROR_INVALID_ARGUMENT;
}
}
// app.<index>.* blocks: 0-indexed, contiguous - the first missing "app.<index>.id" ends the list.
size_t count = 0;
while (properties.contains(std::format("app.{}.id", count))) {
count++;
// Bounded here, not just against the caller's bindings_capacity below: a caller that
// sizes its own buffer off PackageManifest::app_manifest_count (see
// app_package_manifest_parse_into()) would otherwise let a malicious/malformed manifest
// demand an unbounded allocation.
if (count > PACKAGE_MANIFEST_MAX_APP_MANIFESTS) {
LOG_E(TAG, "Manifest declares more than %d apps", PACKAGE_MANIFEST_MAX_APP_MANIFESTS);
return ERROR_BUFFER_OVERFLOW;
}
}
out_package.app_manifest_count = static_cast<uint32_t>(count);
// Catch a gap the count loop above would otherwise silently drop, e.g. app.0.* + app.3.*.
for (const auto& [key, value] : properties) {
auto index = parse_app_key_index(key);
if (index.has_value() && *index >= count) {
LOG_E(TAG, "Manifest declares %s but only %zu contiguous app(s) starting at app.0 were found", key.c_str(), count);
return ERROR_INVALID_ARGUMENT;
}
}
if (count == 0 || bindings_capacity == 0) {
return ERROR_NONE;
}
if (count > bindings_capacity) {
LOG_E(TAG, "Manifest declares %zu apps, capacity is %zu", count, bindings_capacity);
return ERROR_BUFFER_OVERFLOW;
}
for (size_t i = 0; i < count; i++) {
error_t result = parse_app_manifest(properties, i, out_bindings[i]);
if (result != ERROR_NONE) {
return result;
}
}
return ERROR_NONE;
}
+27 -1
View File
@@ -6,6 +6,27 @@
#include <tactility/paths.h>
#include <cstdio>
#include <string>
namespace {
// manifest.location.location is the fully-resolved binary file path
// ({install_dir}/bin/<platform>/<binary>.{elf,so} - see app_resolve_binary_path()), not the
// install directory itself - strip that fixed 3-segment suffix to recover it.
bool app_get_install_dir_from_binary_path(const char* binary_path, std::string& out_install_dir) {
std::string path = binary_path;
for (int i = 0; i < 3; i++) {
auto separator = path.find_last_of('/');
if (separator == std::string::npos) {
return false;
}
path = path.substr(0, separator);
}
out_install_dir = path;
return true;
}
} // namespace
extern "C" {
@@ -46,7 +67,12 @@ error_t app_paths_get_assets_directory(const char* app_id, char* out_path, size_
return ERROR_NOT_FOUND;
}
int written = std::snprintf(out_path, out_path_size, "%s/assets", static_cast<const char*>(manifest.location.location));
std::string install_dir;
if (!app_get_install_dir_from_binary_path(static_cast<const char*>(manifest.location.location), install_dir)) {
return ERROR_NOT_FOUND;
}
int written = std::snprintf(out_path, out_path_size, "%s/assets", install_dir.c_str());
if (written < 0 || (size_t)written >= out_path_size) {
return ERROR_BUFFER_OVERFLOW;
}
+1 -1
View File
@@ -303,7 +303,7 @@ error_t app_scheduler_start(AppInstanceId app_instance_id, AppLocation location,
return ERROR_OUT_OF_MEMORY;
}
// Same bound app_metadata_parse() enforces on manifest.properties-declared depths - a
// Same bound package_manifest_parse() enforces on manifest.properties-declared depths - a
// manifest built directly in C++ (not parsed from a file) must be held to it too.
if (stack.depth > APP_STACK_SIZE_MAX) {
LOG_E(TAG, "[instance %lu] stack depth %u exceeds APP_STACK_SIZE_MAX(%u)", app_instance_id, stack.depth, APP_STACK_SIZE_MAX);