Merge remote-tracking branch 'upstream/main' into feature/es3c28p-es3c35p-modern
This commit is contained in:
@@ -16,6 +16,8 @@
|
||||
#include <esp_elf.h>
|
||||
#include <esp_err.h>
|
||||
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <new>
|
||||
#include <string>
|
||||
@@ -64,13 +66,22 @@ error_t read_file(const char* path, uint8_t** out_data, size_t* out_size) {
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
// location.location can be either an app's install directory or the .elf file directly; the
|
||||
// former resolves to the per-target binary at {dir}/elf/{CONFIG_IDF_TARGET}.elf.
|
||||
bool is_regular_file(const std::string& path) {
|
||||
struct stat path_stat {};
|
||||
return ::stat(path.c_str(), &path_stat) == 0 && S_ISREG(path_stat.st_mode);
|
||||
}
|
||||
|
||||
// location.location can be either an app's install directory or the .elf file directly. A
|
||||
// "packaged" app's install directory always holds its single binary at the fixed path
|
||||
// {dir}/bin/{CONFIG_IDF_TARGET}/app.elf - a "terminal" app has no such file (its several
|
||||
// binaries keep their own names), so this correctly leaves it unresolvable - terminal apps
|
||||
// aren't run through AppLoaderApi (see app/install.h).
|
||||
std::string resolve_elf_path(const std::string& path) {
|
||||
if (path.ends_with(".elf")) {
|
||||
return path;
|
||||
}
|
||||
return path + "/elf/" + CONFIG_IDF_TARGET + ".elf";
|
||||
std::string candidate = path + "/bin/" CONFIG_IDF_TARGET "/app.elf";
|
||||
return is_regular_file(candidate) ? candidate : "";
|
||||
}
|
||||
|
||||
constexpr ElfRequirements EXECUTABLE_REQUIREMENTS = {
|
||||
|
||||
@@ -9,6 +9,7 @@ tactility_add_module(app-module
|
||||
PRIV_INCLUDE_DIRS private/
|
||||
INCLUDE_DIRS include/
|
||||
REQUIRES TactilityKernel service-module minitar
|
||||
PRIV_REQUIRES TactilityKernelCpp
|
||||
)
|
||||
|
||||
# Tells source/io.cpp its real-syscall fallback must go through __real_read/write/close()
|
||||
@@ -19,3 +20,11 @@ if (NOT APPLE)
|
||||
tactility_get_module_name(app-module MODULE_NAME)
|
||||
target_compile_definitions(${MODULE_NAME} PRIVATE TT_APP_IO_WRAPS_STDIO)
|
||||
endif ()
|
||||
|
||||
# install.cpp resolves an installed binary's fixed bin/<platform>/<binary>.so path itself, so it
|
||||
# needs the same platform-arch string app-posix-module's own CMakeLists.txt defines for its
|
||||
# loader (the ESP32 equivalent, CONFIG_IDF_TARGET, comes for free from sdkconfig.h there).
|
||||
if (NOT ESP_PLATFORM)
|
||||
tactility_get_module_name(app-module MODULE_NAME)
|
||||
target_compile_definitions(${MODULE_NAME} PRIVATE "TACTILITY_POSIX_ARCH=\"${CMAKE_SYSTEM_PROCESSOR}\"")
|
||||
endif ()
|
||||
|
||||
@@ -12,8 +12,8 @@ extern "C" {
|
||||
/**
|
||||
* Computes the install directory for @a app_id (does not check whether anything is actually
|
||||
* installed there).
|
||||
* @param[out] path always NULL-terminated on return, even on failure (empty string if
|
||||
* @a path_size == 0 - nothing is written in that case; otherwise at least "" is written)
|
||||
* @param[out] path always NULL-terminated on return, even on failure. Empty when @a path_size
|
||||
* is 0, since nothing is written in that case.
|
||||
* @retval ERROR_NONE on success
|
||||
* @retval ERROR_BUFFER_OVERFLOW @a path_size is too small to hold the path (including the
|
||||
* NULL terminator)
|
||||
@@ -22,14 +22,15 @@ extern "C" {
|
||||
error_t app_get_install_path(const char* app_id, char* path, size_t path_size);
|
||||
|
||||
/**
|
||||
* Installs an app from a tarball at @a source_path: extracts it into the app install directory,
|
||||
* parses the extracted manifest.properties (see app/metadata.h) to determine its id, then
|
||||
* registers it with app_manager_add() as an AppLocation{APP_LOCATION_PATH, <install dir>} app.
|
||||
* If an app with the same id is already installed (via a previous app_install() call), it is
|
||||
* uninstalled first - stopped if running, its old install directory removed - before the new
|
||||
* one takes its place.
|
||||
* @param[in] source_path path to a tar file containing the app (must have manifest.properties
|
||||
* at its root)
|
||||
* Installs a package from a tarball at @a source_path: extracts it into the package's install
|
||||
* directory, parses the extracted manifest.properties (see app/package_manifest.h) into a
|
||||
* PackageManifest and one or more AppManifestBindings, then registers each with app_manager_add()
|
||||
* as an AppLocation{APP_LOCATION_PATH, <binary path>} app.
|
||||
* If a package with the same id is already installed (via a previous app_install() call), it is
|
||||
* uninstalled first: every one of its apps stopped if running, then its old install directory
|
||||
* removed, before the new one takes its place.
|
||||
* @param[in] source_path path to a tar file containing the package (must have
|
||||
* manifest.properties at its root)
|
||||
* @retval ERROR_NONE on success
|
||||
* @retval ERROR_NOT_FOUND @a source_path doesn't exist / can't be read
|
||||
* @retval ERROR_INVALID_ARGUMENT the tarball has no valid manifest.properties at its root
|
||||
@@ -37,11 +38,11 @@ error_t app_get_install_path(const char* app_id, char* path, size_t path_size);
|
||||
error_t app_install(const char* source_path);
|
||||
|
||||
/**
|
||||
* Uninstalls a previously app_install()-ed app: stops it if currently running, deletes its
|
||||
* install directory, and unregisters it (app_manager_remove()).
|
||||
* @param[in] app_id the id the app was installed under (AppMetadata::app_id)
|
||||
* Uninstalls a previously app_install()-ed package: stops every one of its apps if currently
|
||||
* running, deletes its install directory, and unregisters all of them (app_manager_remove()).
|
||||
* @param[in] app_id the package id it was installed under (PackageManifest::id)
|
||||
* @retval ERROR_NONE on success
|
||||
* @retval ERROR_NOT_FOUND no such app was installed via app_install()
|
||||
* @retval ERROR_NOT_FOUND no such package was installed via app_install()
|
||||
*/
|
||||
error_t app_uninstall(const char* app_id);
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include "location.h"
|
||||
#include <app/manifest.h>
|
||||
#include <tactility/error.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include "location.h"
|
||||
#include <tactility/error.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <app/instance.h>
|
||||
#include <app/manifest.h>
|
||||
#include <app/package_manifest.h>
|
||||
#include <app/stream.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
@@ -44,6 +45,48 @@ error_t app_manager_find_manifest(const char* id, struct AppManifest* out_manife
|
||||
typedef void (*AppManifestVisitorFn)(const struct AppManifest* manifest, void* context);
|
||||
void app_manager_for_each_manifest(AppManifestVisitorFn visitor, void* context);
|
||||
|
||||
/**
|
||||
* Registers a package for enumeration via app_manager_for_each_package(). Separate from
|
||||
* registering its apps - the caller still calls app_manager_add() for each AppManifest.
|
||||
* @param[in] app_ids the ids of the AppManifest(s) this package registered
|
||||
* @retval ERROR_INVALID_ARGUMENT a package with the same id is already registered
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t app_manager_add_package(const struct PackageManifest* package, const char* const* app_ids, size_t app_id_count);
|
||||
|
||||
/**
|
||||
* Unregisters a previously-added package. Does not touch its apps' own registrations.
|
||||
* @retval ERROR_NOT_FOUND no package with this id is registered
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t app_manager_remove_package(const char* package_id);
|
||||
|
||||
/**
|
||||
* @param[out] out_package set to a copy of the package on success
|
||||
* @retval ERROR_NOT_FOUND no package with this id is registered
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t app_manager_find_package(const char* package_id, struct PackageManifest* out_package);
|
||||
|
||||
/** One registered package, handed to AppPackageVisitorFn - see app_manager_for_each_package(). */
|
||||
struct AppPackage {
|
||||
struct PackageManifest package;
|
||||
/** How many entries @a app_ids points to. */
|
||||
size_t app_id_count;
|
||||
/** Valid only for the duration of the app_manager_for_each_package() call that produced
|
||||
* this - copy out what's needed before returning from the visitor. */
|
||||
const char* const* app_ids;
|
||||
};
|
||||
|
||||
typedef void (*AppPackageVisitorFn)(const struct AppPackage* pkg, void* context);
|
||||
|
||||
/**
|
||||
* Calls @a visitor once for every registered package. Iteration order is unspecified.
|
||||
* @warning Same threading contract as app_manager_for_each_manifest(): runs with an internal
|
||||
* lock held - do not call any app_manager_*() function from inside @a visitor.
|
||||
*/
|
||||
void app_manager_for_each_package(AppPackageVisitorFn visitor, void* context);
|
||||
|
||||
/** One fd-to-stream binding for app_start_with_streams() (app/start.h). Every field is passed
|
||||
* through to app_stream_subscribe() as-is; see its own doc for the ownership contracts. */
|
||||
struct AppStreamBinding {
|
||||
@@ -90,8 +133,8 @@ error_t app_manager_get_topmost_app_id(char* buffer, size_t buffer_size);
|
||||
|
||||
/**
|
||||
* Registers @a path as a directory to scan for app manifests - each direct subdirectory of
|
||||
* @a path is expected to hold a manifest.properties (see app/metadata.h), matching the layout
|
||||
* app_install() creates ({install dir}/{app_id}/manifest.properties), though this is not
|
||||
* @a path is expected to hold a manifest.properties (see app/package_manifest.h), matching the
|
||||
* layout app_install() creates ({install dir}/{package id}/manifest.properties), though this is not
|
||||
* install/uninstall - it only ever adds/removes manifest registrations, never touches files on
|
||||
* disk or running instances. No-op if @a path is already registered. Does not scan immediately -
|
||||
* call app_manager_install_path_scan() to do that.
|
||||
|
||||
@@ -11,7 +11,10 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
// Character count, excluding null terminator
|
||||
#define APP_ID_LENGTH 32
|
||||
#define APP_MANIFEST_ID_LENGTH 32
|
||||
|
||||
// Character count, excluding null terminator
|
||||
#define APP_MANIFEST_NAME_LENGTH 32
|
||||
|
||||
/** Broad classification of an app, used for grouping/launcher presentation. */
|
||||
enum AppCategory {
|
||||
@@ -45,10 +48,10 @@ struct AppStackConfig {
|
||||
|
||||
/** Describes a registrable app. One manifest exists per app id. */
|
||||
struct AppManifest {
|
||||
/** Unique app identifier. Should never be NULL. */
|
||||
const char* id;
|
||||
/** Human-readable name. Should never be NULL. */
|
||||
const char* name;
|
||||
/** Unique app identifier. Must be NULL-terminated. */
|
||||
char id[APP_MANIFEST_ID_LENGTH + 1];
|
||||
/** Human-readable name. Must be NULL-terminated. */
|
||||
char name[APP_MANIFEST_NAME_LENGTH + 1];
|
||||
enum AppCategory category;
|
||||
struct AppLocation location;
|
||||
/** Bitmask of AppManifestFlags. Most apps should leave this 0. */
|
||||
@@ -57,7 +60,9 @@ struct AppManifest {
|
||||
struct AppStackConfig stack;
|
||||
};
|
||||
|
||||
bool app_id_is_valid(const char* id);
|
||||
bool app_manifest_id_is_valid(const char* id);
|
||||
bool app_manifest_name_is_valid(const char* name);
|
||||
bool app_manifest_stack_size_is_valid(const char* value);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/error.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define APP_METADATA_TARGET_SDK_LENGTH 16
|
||||
#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_ID_LENGTH 64
|
||||
|
||||
struct AppMetadata {
|
||||
|
||||
/**
|
||||
* The SDK version that was used to compile this app. (e.g. "0.6.0")
|
||||
* Must be NULL-terminated.
|
||||
*/
|
||||
char target_sdk[APP_METADATA_TARGET_SDK_LENGTH + 1];
|
||||
|
||||
/**
|
||||
* The identifier by which the app is launched by the system and other apps.
|
||||
* Must be NULL-terminated.
|
||||
*/
|
||||
char app_id[APP_METADATA_APP_ID_LENGTH + 1];
|
||||
|
||||
/**
|
||||
* The user-readable name of the app. Used in UI.
|
||||
* Must be NULL-terminated.
|
||||
*/
|
||||
char app_name[APP_METADATA_APP_NAME_LENGTH + 1];
|
||||
|
||||
/**
|
||||
* The version as it is displayed to the user (e.g. "1.2.0")
|
||||
* Must be NULL-terminated.
|
||||
*/
|
||||
char app_version_name[APP_METADATA_APP_VERSION_NAME_LENGTH + 1];
|
||||
|
||||
/** The technical version (must be incremented with new releases of the app) */
|
||||
uint64_t app_version_code;
|
||||
|
||||
/**
|
||||
* 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];
|
||||
|
||||
/**
|
||||
* Stack depth (in words) for the app's task. Optional; 0 means scheduler default.
|
||||
* @warning Avoid default values: the default is conservative, which wastes memory.
|
||||
*/
|
||||
uint32_t stack_depth;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a manifest.properties file at @a path into @a out_metadata, auto-detecting the V1
|
||||
* (sectioned, e.g. "[app]id=...") or V2 (flat dot-notation, e.g. "app.id=...") format from its
|
||||
* first line.
|
||||
* @retval ERROR_NONE on success
|
||||
* @retval ERROR_NOT_FOUND the file doesn't exist / couldn't be opened
|
||||
* @retval ERROR_INVALID_ARGUMENT the file isn't a valid manifest, or a field's value doesn't fit
|
||||
* @a out_metadata's fixed-size buffers
|
||||
*/
|
||||
error_t app_metadata_parse(const char* path, struct AppMetadata* out_metadata);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,88 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <app/manifest.h>
|
||||
|
||||
#include <tactility/error.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define PACKAGE_MANIFEST_TARGET_SDK_LENGTH 16
|
||||
#define PACKAGE_MANIFEST_ID_LENGTH 32
|
||||
#define PACKAGE_MANIFEST_VERSION_NAME_LENGTH 16
|
||||
#define PACKAGE_MANIFEST_REQUIRES_DEVICE_ID_LENGTH 64
|
||||
|
||||
/** Character count, excluding null terminator, for AppManifestBinding::binary. */
|
||||
#define APP_MANIFEST_BINARY_LENGTH 31
|
||||
|
||||
/** Largest number of AppManifest entries package_manifest_parse() can produce from a single
|
||||
* manifest.properties file. */
|
||||
#define PACKAGE_MANIFEST_MAX_APP_MANIFESTS 32
|
||||
|
||||
/** A package-level manifest.properties: SDK/version metadata that applies to the whole package,
|
||||
* not to any one of its (possibly several) apps. Not kept in memory at runtime - only used to
|
||||
* create and cache the AppManifest(s) it describes. */
|
||||
struct PackageManifest {
|
||||
/**
|
||||
* The package identifier (e.g. the install directory name). Distinct from any of its own
|
||||
* AppManifest ids.
|
||||
* Must be NULL-terminated.
|
||||
*/
|
||||
char id[PACKAGE_MANIFEST_ID_LENGTH + 1];
|
||||
|
||||
/**
|
||||
* The package version as it is displayed to the user (e.g. "1.2.0")
|
||||
* Must be NULL-terminated.
|
||||
*/
|
||||
char version_name[PACKAGE_MANIFEST_VERSION_NAME_LENGTH + 1];
|
||||
|
||||
/** The package's technical version (must be incremented with new releases). */
|
||||
uint64_t version_code;
|
||||
|
||||
/**
|
||||
* The SDK version that was used to compile this package. (e.g. "0.6.0")
|
||||
* Must be NULL-terminated.
|
||||
*/
|
||||
char target_sdk[PACKAGE_MANIFEST_TARGET_SDK_LENGTH + 1];
|
||||
|
||||
/**
|
||||
* Comma-separated list of device ids the package is restricted to (e.g. "m5stack-tab5"),
|
||||
* matching the folder names under Devices/. Empty means unrestricted.
|
||||
* Must be NULL-terminated.
|
||||
*/
|
||||
char requires_device_id[PACKAGE_MANIFEST_REQUIRES_DEVICE_ID_LENGTH + 1];
|
||||
|
||||
/** How many AppManifest entries this package's manifest.properties declared. */
|
||||
uint32_t app_manifest_count;
|
||||
};
|
||||
|
||||
/** Pairs a parsed AppManifest with the filename (without extension) it installs as under
|
||||
* bin/<platform>/ - e.g. "main" resolves to bin/posix-x86_64/main.so. Not kept anywhere at
|
||||
* runtime - same lifetime as PackageManifest, only used to hand parse results to the installer/
|
||||
* scanner, which resolve `binary` into AppManifest::location::location. */
|
||||
struct AppManifestBinding {
|
||||
struct AppManifest manifest;
|
||||
char binary[APP_MANIFEST_BINARY_LENGTH + 1];
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a manifest.properties file at @a path (flat dot-notation, e.g. "app.id=...") into
|
||||
* @a out_package and @a out_bindings.
|
||||
* @param[out] out_bindings written with up to @a bindings_capacity entries (see
|
||||
* PackageManifest::app_manifest_count for how many)
|
||||
* @param[in] bindings_capacity the capacity of @a out_bindings
|
||||
* @retval ERROR_NONE on success
|
||||
* @retval ERROR_NOT_FOUND the file doesn't exist / couldn't be opened
|
||||
* @retval ERROR_INVALID_ARGUMENT the file isn't a valid manifest, or a field's value doesn't fit
|
||||
* its fixed-size buffer
|
||||
* @retval ERROR_BUFFER_OVERFLOW the manifest declares more apps than @a bindings_capacity
|
||||
*/
|
||||
error_t app_package_manifest_parse(const char* path, struct PackageManifest* out_package, struct AppManifestBinding* out_bindings, size_t bindings_capacity);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,22 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
#include <sdkconfig.h>
|
||||
#endif
|
||||
|
||||
#include <string>
|
||||
|
||||
// Resolves an AppManifestBinding::binary filename (without extension) to its installed path
|
||||
// under this fixed, predictable location, mirroring what each platform's own loader
|
||||
// (app_posix_loader_service.cpp / app_esp32_loader_service.cpp) already resolves a plain
|
||||
// ".so"/".elf" AppLocation::location straight through unchanged - so setting location.location
|
||||
// to this exact path means neither loader needs to guess which of a package's several binaries
|
||||
// an AppManifest refers to.
|
||||
inline std::string app_resolve_binary_path(const std::string& install_dir, const std::string& binary) {
|
||||
#ifdef ESP_PLATFORM
|
||||
return install_dir + "/bin/" CONFIG_IDF_TARGET "/" + binary + ".elf";
|
||||
#else
|
||||
return install_dir + "/bin/posix-" TACTILITY_POSIX_ARCH "/" + binary + ".so";
|
||||
#endif
|
||||
}
|
||||
@@ -3,8 +3,11 @@
|
||||
|
||||
#include <app/instance.h>
|
||||
#include <app/manifest.h>
|
||||
#include <app/package_manifest.h>
|
||||
#include <app/private/fd_table.h>
|
||||
|
||||
#include <TactilityCpp/Allocator.h>
|
||||
|
||||
#include <tactility/concurrent/mutex.h>
|
||||
#include <tactility/freertos/freertos.h>
|
||||
#include <tactility/freertos/semphr.h>
|
||||
@@ -13,6 +16,7 @@
|
||||
#include <stdint.h>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
/**
|
||||
* A dedicated completion signal(1) for one app instance's task, given as the
|
||||
@@ -57,8 +61,16 @@ struct AppInstanceRecord {
|
||||
AppFdTable fd_table {};
|
||||
};
|
||||
|
||||
/** A registered installed package - see app_manager_add_package() (app/manager.h). */
|
||||
struct AppPackageRecord {
|
||||
struct PackageManifest package;
|
||||
std::vector<std::string> app_ids;
|
||||
};
|
||||
|
||||
struct AppLedger {
|
||||
std::unordered_map<std::string, const AppManifest*> manifests;
|
||||
// OptExternalAllocator: bigger entries than `manifests`, and unlike `instances` isn't on the app start/stop hot path.
|
||||
std::unordered_map<std::string, AppPackageRecord, std::hash<std::string>, std::equal_to<std::string>, tt::OptExternalAllocator<std::pair<const std::string, AppPackageRecord>>> packages;
|
||||
std::unordered_map<uint32_t, AppInstanceRecord> instances;
|
||||
uint32_t next_instance_id = 1;
|
||||
Mutex mutex {};
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <app/metadata.h>
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
/** Shared helpers + per-format parsers for app_metadata_parse() (source/app_metadata_parsing.cpp)
|
||||
* - split out like the old tt::app manifest parser (AppManifestParsing/V1/V2.cpp) that this is
|
||||
* modelled on, one file per format plus a shared dispatcher. */
|
||||
|
||||
bool app_metadata_get_value(const std::map<std::string, std::string>& properties, const std::string& key, std::string& out_value);
|
||||
|
||||
bool app_metadata_is_valid_format_version(const std::string& version);
|
||||
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);
|
||||
bool app_metadata_is_valid_stack_size(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 */
|
||||
bool app_metadata_copy_bounded(char* dest, size_t dest_size, const std::string& value);
|
||||
|
||||
/** Parses a V1 (sectioned INI, e.g. "[app]versionName=...") manifest map into @a out_metadata. */
|
||||
bool app_metadata_parse_v1(const std::map<std::string, std::string>& properties, struct AppMetadata& out_metadata);
|
||||
|
||||
/** Parses a V2 (flat dot-notation, e.g. "app.version.name=...") manifest map into @a out_metadata. */
|
||||
bool app_metadata_parse_v2(const std::map<std::string, std::string>& properties, struct AppMetadata& out_metadata);
|
||||
|
||||
bool app_metadata_validate_string(const std::string& value, bool (*is_valid_char)(char));
|
||||
@@ -0,0 +1,56 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <app/package_manifest.h>
|
||||
|
||||
#include <TactilityCpp/Allocator.h>
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
/** Shared helpers + parser for app_package_manifest_parse() (source/package_manifest_parsing.cpp). */
|
||||
|
||||
bool app_package_manifest_get_value(const std::map<std::string, std::string>& properties, const std::string& key, std::string& out_value);
|
||||
|
||||
bool app_package_manifest_is_valid_format_version(const std::string& version);
|
||||
bool app_package_manifest_is_valid_version_name(const std::string& version);
|
||||
bool app_package_manifest_is_valid_version_code(const std::string& version);
|
||||
bool app_package_manifest_is_valid_bool(const std::string& value);
|
||||
|
||||
/** Validates a comma-separated list of device ids (alphanumeric + '-' items, matching Devices/<id> folder names). */
|
||||
bool app_package_manifest_is_valid_device_id_list(const std::string& value);
|
||||
|
||||
/** Validates a binary filename stem (AppManifestBinding::binary): alphanumeric plus '.', '_', '-'. */
|
||||
bool app_package_manifest_is_valid_binary_name(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 */
|
||||
bool app_package_manifest_copy_bounded(char* dest, size_t dest_size, const std::string& value);
|
||||
|
||||
/** Parses a V2 (flat dot-notation, e.g. "app.version.name=...") manifest map into @a out_package
|
||||
* and (unless @a bindings_capacity is 0) its single AppManifestBinding, out_bindings[0].
|
||||
* @retval ERROR_NONE on success
|
||||
* @retval ERROR_INVALID_ARGUMENT a required key is missing or a value doesn't fit/validate
|
||||
* @retval ERROR_BUFFER_OVERFLOW @a bindings_capacity is nonzero but smaller than needed */
|
||||
error_t package_manifest_parse_v2(const std::map<std::string, std::string>& properties, struct PackageManifest& out_package, struct AppManifestBinding* out_bindings, size_t bindings_capacity);
|
||||
|
||||
/** Parses a V3 (flat dot-notation with 0-indexed "app.N.*" blocks) manifest map into
|
||||
* @a out_package and up to @a bindings_capacity AppManifestBinding entries.
|
||||
* @retval ERROR_NONE on success
|
||||
* @retval ERROR_INVALID_ARGUMENT a required key is missing or a value doesn't fit/validate
|
||||
* @retval ERROR_BUFFER_OVERFLOW @a bindings_capacity is nonzero but smaller than the
|
||||
* manifest's app_manifest_count */
|
||||
error_t package_manifest_parse_v3(const std::map<std::string, std::string>& properties, struct PackageManifest& out_package, struct AppManifestBinding* out_bindings, size_t bindings_capacity);
|
||||
|
||||
bool app_package_manifest_validate_string(const std::string& value, bool (*is_valid_char)(char));
|
||||
|
||||
/** Convenience wrapper around app_package_manifest_parse(): parses @a path once to learn
|
||||
* PackageManifest::app_manifest_count, then resizes @a out_bindings to fit exactly and parses
|
||||
* again to fill it - so the caller never needs to pre-allocate a fixed maximum.
|
||||
* @retval ERROR_NONE on success
|
||||
* @retval ERROR_NOT_FOUND the file doesn't exist / couldn't be opened
|
||||
* @retval ERROR_INVALID_ARGUMENT the file isn't a valid manifest, or a field's value doesn't fit
|
||||
* its fixed-size buffer */
|
||||
error_t app_package_manifest_parse_into(const char* path, struct PackageManifest& out_package, std::vector<struct AppManifestBinding, tt::OptExternalAllocator<struct AppManifestBinding>>& out_bindings);
|
||||
@@ -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(®istry.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(®istry.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(®istry.mutex);
|
||||
|
||||
return add_result;
|
||||
|
||||
@@ -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(®istry.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(®istry.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(®istry.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(®istry.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(®istry.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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -73,7 +73,11 @@ bool wait_for_state(AppInstanceId id, AppInstanceState target, uint32_t timeout_
|
||||
|
||||
AppInstanceId start_idle_app(const char* id) {
|
||||
ensure_memory_loader_registered();
|
||||
AppManifest manifest { id, id, APP_CATEGORY_USER, { APP_LOCATION_MEMORY, reinterpret_cast<void*>(idle_app_main) } };
|
||||
AppManifest manifest {};
|
||||
std::strncpy(manifest.id, id, sizeof(manifest.id) - 1);
|
||||
std::strncpy(manifest.name, id, sizeof(manifest.name) - 1);
|
||||
manifest.category = APP_CATEGORY_USER;
|
||||
manifest.location = { APP_LOCATION_MEMORY, reinterpret_cast<void*>(idle_app_main) };
|
||||
REQUIRE_EQ(app_manager_add(&manifest), ERROR_NONE);
|
||||
AppInstanceId instance_id = 0;
|
||||
REQUIRE_EQ(app_start(id, 0, nullptr, &instance_id), ERROR_NONE);
|
||||
|
||||
@@ -58,19 +58,21 @@ bool is_executable_file(const std::string& resolved_path) {
|
||||
#endif
|
||||
}
|
||||
|
||||
// location.location can be either an app's install directory or the .so file directly; the
|
||||
// former resolves to the per-architecture binary at {dir}/elf/posix-{TACTILITY_POSIX_ARCH}.so,
|
||||
// mirroring app_esp32_loader_service.cpp's resolve_elf_path().
|
||||
// location.location can be either an app's install directory or the .so file directly, mirroring
|
||||
// app_esp32_loader_service.cpp's resolve_elf_path(). A "packaged" app's install directory always
|
||||
// holds its single binary at the fixed path {dir}/bin/posix-{TACTILITY_POSIX_ARCH}/app.so - a
|
||||
// "terminal" app has no such file (its several binaries keep their own names), so this correctly
|
||||
// leaves it unresolvable - terminal apps aren't run through AppLoaderApi (see app/install.h).
|
||||
error_t resolve_app_path(const std::string& path, std::string& resolvedPath) {
|
||||
if (path.ends_with(".so")) {
|
||||
resolvedPath = path;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
std::string shared_object_path = path + "/elf/posix-" TACTILITY_POSIX_ARCH ".so";
|
||||
if (!is_regular_file(shared_object_path)) {
|
||||
std::string candidate = path + "/bin/posix-" TACTILITY_POSIX_ARCH "/app.so";
|
||||
if (!is_regular_file(candidate)) {
|
||||
return ERROR_NOT_FOUND;
|
||||
}
|
||||
resolvedPath = shared_object_path;
|
||||
resolvedPath = candidate;
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
@@ -94,9 +96,6 @@ error_t api_load(AppLocation location, AppRuntime* out_runtime) {
|
||||
|
||||
LOG_I(TAG, "Loading %s", app_path.c_str());
|
||||
|
||||
// RTLD_NOW: a missing symbol fails here, not mid-run(). RTLD_LOCAL: this app's own exported
|
||||
// symbols (if any beyond its entry point) don't leak into the process's global scope and
|
||||
// clash with a different app's.
|
||||
void* handle = dlopen(app_path.c_str(), RTLD_NOW | RTLD_LOCAL);
|
||||
if (handle == nullptr) {
|
||||
LOG_E(TAG, "dlopen(%s) failed: %s", app_path.c_str(), dlerror());
|
||||
@@ -117,8 +116,8 @@ error_t api_load(AppLocation location, AppRuntime* out_runtime) {
|
||||
int32_t api_run(AppRuntime runtime_ptr, uint32_t /*app_instance_id*/, int argc, char* argv[]) {
|
||||
auto* runtime = static_cast<PosixAppRuntime*>(runtime_ptr);
|
||||
|
||||
dlerror(); // clear any pending error, per dlsym(3)'s own recommended idiom for telling a NULL
|
||||
// symbol address apart from a real lookup failure
|
||||
// Clear any pending error, per dlsym(3)'s own recommended idiom for telling a NULL symbol address apart from a real lookup failure
|
||||
dlerror();
|
||||
void* symbol = dlsym(runtime->handle, "main");
|
||||
const char* lookup_error = dlerror();
|
||||
if (symbol == nullptr || lookup_error != nullptr) {
|
||||
|
||||
@@ -15,14 +15,27 @@ set_target_properties(app_posix_module_test_fixture PROPERTIES POSITION_INDEPEND
|
||||
set(NON_ELF_FIXTURE_PATH "${CMAKE_CURRENT_BINARY_DIR}/not-elf.so")
|
||||
file(WRITE "${NON_ELF_FIXTURE_PATH}" "not an elf file")
|
||||
|
||||
# An install-directory-shaped fixture: {dir}/bin/posix-{arch}/app.so, matching where
|
||||
# app_posix_loader_service.cpp's resolve_app_path() looks for a "packaged" app's single binary.
|
||||
set(INSTALL_DIR_FIXTURE_PATH "${CMAKE_CURRENT_BINARY_DIR}/install-dir-fixture")
|
||||
set(INSTALL_DIR_FIXTURE_BIN_DIR "${INSTALL_DIR_FIXTURE_PATH}/bin/posix-${CMAKE_SYSTEM_PROCESSOR}")
|
||||
add_custom_command(
|
||||
OUTPUT "${INSTALL_DIR_FIXTURE_BIN_DIR}/app.so"
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${INSTALL_DIR_FIXTURE_BIN_DIR}"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy "$<TARGET_FILE:app_posix_module_test_fixture>" "${INSTALL_DIR_FIXTURE_BIN_DIR}/app.so"
|
||||
DEPENDS app_posix_module_test_fixture
|
||||
)
|
||||
add_custom_target(install_dir_fixture DEPENDS "${INSTALL_DIR_FIXTURE_BIN_DIR}/app.so")
|
||||
|
||||
file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/source/*.cpp)
|
||||
add_executable(AppPosixModuleTests EXCLUDE_FROM_ALL ${TEST_SOURCES})
|
||||
add_dependencies(AppPosixModuleTests app_posix_module_test_fixture)
|
||||
add_dependencies(AppPosixModuleTests app_posix_module_test_fixture install_dir_fixture)
|
||||
|
||||
target_include_directories(AppPosixModuleTests PRIVATE ${DOCTESTINC})
|
||||
target_compile_definitions(AppPosixModuleTests PRIVATE
|
||||
FIXTURE_APP_PATH="$<TARGET_FILE:app_posix_module_test_fixture>"
|
||||
FIXTURE_NON_ELF_PATH="${NON_ELF_FIXTURE_PATH}"
|
||||
FIXTURE_INSTALL_DIR_PATH="${INSTALL_DIR_FIXTURE_PATH}"
|
||||
)
|
||||
|
||||
add_test(NAME AppPosixModuleTests COMMAND AppPosixModuleTests)
|
||||
|
||||
@@ -148,6 +148,12 @@ TEST_CASE("app_is_executable() rejects a nonexistent path") {
|
||||
TEST_CASE("app_is_executable() rejects an install-directory-shaped path missing its per-arch .so") {
|
||||
ensure_path_loader_registered();
|
||||
|
||||
// FIXTURE_DIR itself has no elf/posix-<arch>.so under it, so resolution fails.
|
||||
// FIXTURE_DIR itself has no bin/posix-<arch>/app.so under it, so resolution fails.
|
||||
CHECK_FALSE(is_executable_path(FIXTURE_DIR.c_str()));
|
||||
}
|
||||
|
||||
TEST_CASE("app_is_executable() accepts an install-directory-shaped path with bin/<arch>/app.so") {
|
||||
ensure_path_loader_registered();
|
||||
|
||||
CHECK(is_executable_path(FIXTURE_INSTALL_DIR_PATH));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user