mirror of
https://github.com/ByteWelder/Tactility.git
synced 2026-10-17 10:55:06 +00:00
Make it possible to run elf/so files directly
This commit is contained in:
parent
643cbc3806
commit
92e5c9bf43
@ -3,6 +3,8 @@
|
||||
#include <sdkconfig.h>
|
||||
#endif
|
||||
|
||||
#include <app/elf_check.h>
|
||||
#include <app/exec.h>
|
||||
#include <app/loader.h>
|
||||
#include <app/location.h>
|
||||
|
||||
@ -72,6 +74,28 @@ std::string resolve_elf_path(const std::string& path) {
|
||||
return path + "/elf/" + CONFIG_IDF_TARGET + ".elf";
|
||||
}
|
||||
|
||||
constexpr ElfRequirements EXECUTABLE_REQUIREMENTS = {
|
||||
.elf_class = ELF_CLASS_32,
|
||||
.data = ELF_DATA_2LSB,
|
||||
.type = ELF_TYPE_DYN,
|
||||
#if defined(__XTENSA__)
|
||||
.machine = ELF_MACHINE_XTENSA,
|
||||
#elif defined(__riscv)
|
||||
.machine = ELF_MACHINE_RISCV,
|
||||
#else
|
||||
#error "Unsupported ESP32 architecture for ELF machine check"
|
||||
#endif
|
||||
};
|
||||
|
||||
// Validates an already-resolved binary path (see resolve_elf_path()) before it's handed to
|
||||
// esp_elf_relocate(), which performs no header validation of its own: the extension and
|
||||
// directory checks are cheap string comparisons, so the file is only opened as a last resort.
|
||||
bool is_executable_file(const std::string& resolved_path) {
|
||||
return resolved_path.ends_with(".elf")
|
||||
&& app_exec_path_allowed(resolved_path.c_str())
|
||||
&& elf_check_file(resolved_path.c_str(), &EXECUTABLE_REQUIREMENTS);
|
||||
}
|
||||
|
||||
error_t api_load(AppLocation location, AppRuntime* out_runtime) {
|
||||
if (location.type != APP_LOCATION_PATH) {
|
||||
LOG_E(TAG, "Out of memory");
|
||||
@ -88,6 +112,12 @@ error_t api_load(AppLocation location, AppRuntime* out_runtime) {
|
||||
|
||||
auto elf_path = resolve_elf_path(static_cast<const char*>(location.location));
|
||||
|
||||
if (!is_executable_file(elf_path)) {
|
||||
LOG_E(TAG, "Not executable: %s", elf_path.c_str());
|
||||
delete runtime;
|
||||
return ERROR_NOT_ALLOWED;
|
||||
}
|
||||
|
||||
size_t size = 0;
|
||||
error_t read_result = read_file(elf_path.c_str(), &runtime->file_data, &size);
|
||||
if (read_result != ERROR_NONE) {
|
||||
@ -127,10 +157,20 @@ void api_unload(AppRuntime runtime_ptr) {
|
||||
delete runtime;
|
||||
}
|
||||
|
||||
bool api_is_executable(AppLocation location) {
|
||||
if (location.type != APP_LOCATION_PATH) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto elf_path = resolve_elf_path(static_cast<const char*>(location.location));
|
||||
return is_executable_file(elf_path);
|
||||
}
|
||||
|
||||
AppLoaderApi loader_api = {
|
||||
.load = api_load,
|
||||
.run = api_run,
|
||||
.unload = api_unload,
|
||||
.is_executable = api_is_executable,
|
||||
};
|
||||
|
||||
void* create_service(const ServiceManifest*) {
|
||||
|
||||
38
Modules/app-module/include/app/elf_check.h
Normal file
38
Modules/app-module/include/app/elf_check.h
Normal file
@ -0,0 +1,38 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
// <elf.h> is not guaranteed to exist under the ESP32 newlib toolchain
|
||||
#define ELF_CLASS_32 1
|
||||
#define ELF_CLASS_64 2
|
||||
#define ELF_DATA_2LSB 1
|
||||
#define ELF_TYPE_DYN 3
|
||||
#define ELF_MACHINE_XTENSA 94
|
||||
#define ELF_MACHINE_RISCV 243
|
||||
#define ELF_MACHINE_X86_64 62
|
||||
#define ELF_MACHINE_AARCH64 183
|
||||
|
||||
/** What a loader needs an ELF file's header to say before it will try to load it. */
|
||||
struct ElfRequirements {
|
||||
uint8_t elf_class; /**< ELF_CLASS_32 / ELF_CLASS_64 */
|
||||
uint8_t data; /**< ELF_DATA_2LSB */
|
||||
uint16_t type; /**< ELF_TYPE_DYN */
|
||||
uint16_t machine; /**< ELF_MACHINE_XTENSA / _RISCV / _X86_64 / _AARCH64 */
|
||||
};
|
||||
|
||||
/**
|
||||
* Reads the first 20 bytes of @a path (e_ident, e_type, e_machine; identical offsets for
|
||||
* ELF32 and ELF64) and checks the magic number and every field in @a requirements match.
|
||||
* @return false if @a path can't be opened, is too short, or doesn't match
|
||||
*/
|
||||
bool elf_check_file(const char* path, const struct ElfRequirements* requirements);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
57
Modules/app-module/include/app/exec.h
Normal file
57
Modules/app-module/include/app/exec.h
Normal file
@ -0,0 +1,57 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <app/instance.h>
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <tactility/error.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** Largest number of directories app_exec_path_add() can hold at once. */
|
||||
#define APP_EXEC_MAX_PATHS 8
|
||||
|
||||
/**
|
||||
* Registers @a path as a directory whose contents may be executed. Applies recursively to
|
||||
* subdirectories. Registering the same path twice is a no-op.
|
||||
* @retval ERROR_OUT_OF_MEMORY the registry already holds APP_EXEC_MAX_PATHS entries
|
||||
* @retval ERROR_NONE on success (including when @a path was already registered)
|
||||
*/
|
||||
error_t app_exec_path_add(const char* path);
|
||||
|
||||
/**
|
||||
* Unregisters a directory previously passed to app_exec_path_add().
|
||||
* @retval ERROR_NOT_FOUND @a path was not registered
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t app_exec_path_remove(const char* path);
|
||||
|
||||
/**
|
||||
* True when @a file_path lives under a directory registered with app_exec_path_add(), at any
|
||||
* depth. Rejects any path containing a ".." segment rather than normalising it.
|
||||
*/
|
||||
bool app_exec_path_allowed(const char* file_path);
|
||||
|
||||
/**
|
||||
* Asks the registered APP_LOADER_PATH_SERVICE_ID loader (see app/loader.h) whether @a path is
|
||||
* runnable on this target, without loading it.
|
||||
* @return false if @a path can't be run, or if no path loader is registered
|
||||
*/
|
||||
bool app_exec_is_executable_path(const char* path);
|
||||
|
||||
/**
|
||||
* Runs @a path directly, without it having to be pre-registered as an app (e.g. via
|
||||
* app_manager_add() or app_install()), for a file a user picked in a file browser. Internally
|
||||
* registers (app_manager_add(), APP_MANIFEST_FLAG_HIDDEN so it never shows up in app-browsing
|
||||
* UIs) a manifest for @a path the first time it's run, reusing that same registration on
|
||||
* subsequent calls for the same path, then starts it (app_manager_start_with_parameters()).
|
||||
* @retval ERROR_NOT_ALLOWED app_exec_is_executable_path(path) is false
|
||||
* @retval ERROR_NONE on success
|
||||
*/
|
||||
error_t app_exec_run_path(const char* path, int argc, const char* const argv[], AppInstanceId* out_app_instance_id);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@ -3,6 +3,7 @@
|
||||
|
||||
#include <app/manifest.h>
|
||||
#include <tactility/error.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include "location.h"
|
||||
|
||||
@ -18,15 +19,8 @@ extern "C" {
|
||||
* APP_LOCATION_PATH must register under. Implemented by a platform module (e.g. app-esp32-module). */
|
||||
#define APP_LOADER_PATH_SERVICE_ID "app-loader-path"
|
||||
|
||||
/**
|
||||
* Entry point signature for an APP_LOCATION_MEMORY app: a function linked directly into this
|
||||
* firmware binary. Called on the dedicated task app-module's scheduler spawns for this instance,
|
||||
* blocking for the app's whole lifetime - same contract as an external app's main(). Use
|
||||
* app_scheduler_current_app_id() to identify this running instance (e.g. with
|
||||
* app_event_subscribe()/window_manager_create()/etc.). The instance closes when this function
|
||||
* returns - no separate call is needed.
|
||||
* AppManifest::location.location holds this cast to void*.
|
||||
*/
|
||||
/** Entry point signature for an APP_LOCATION_MEMORY app.
|
||||
* AppManifest::location.location holds this cast to void*. */
|
||||
typedef int32_t (*AppMainFn)(int argc, char* argv[]);
|
||||
|
||||
typedef void* AppRuntime;
|
||||
@ -53,6 +47,11 @@ struct AppLoaderApi {
|
||||
|
||||
/** Releases whatever load() allocated. Called after run() returns. */
|
||||
void (*unload)(AppRuntime runtime);
|
||||
|
||||
/**
|
||||
* Reports whether this loader could load and run whatever @a location points at, without actually loading it.
|
||||
*/
|
||||
bool (*is_executable)(struct AppLocation location);
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
16
Modules/app-module/private/app/private/exec_internal.h
Normal file
16
Modules/app-module/private/app/private/exec_internal.h
Normal file
@ -0,0 +1,16 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Registers app-module's default executable directories (data/bin, system/bin, the app install
|
||||
* root) with app_exec_path_add(). Called once from app-module's own Module::start().
|
||||
*/
|
||||
void app_exec_register_default_paths(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
45
Modules/app-module/source/elf_check.cpp
Normal file
45
Modules/app-module/source/elf_check.cpp
Normal file
@ -0,0 +1,45 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app/elf_check.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr size_t ELF_HEADER_PREFIX_SIZE = 20; // e_ident[16] + e_type(2) + e_machine(2)
|
||||
constexpr uint8_t ELF_MAGIC[4] = { 0x7f, 'E', 'L', 'F' };
|
||||
|
||||
uint16_t read_le16(const uint8_t* bytes) {
|
||||
return static_cast<uint16_t>(bytes[0] | (bytes[1] << 8));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool elf_check_file(const char* path, const struct ElfRequirements* requirements) {
|
||||
FILE* file = fopen(path, "rb");
|
||||
if (file == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t header[ELF_HEADER_PREFIX_SIZE];
|
||||
size_t read = fread(header, 1, sizeof(header), file);
|
||||
fclose(file);
|
||||
|
||||
if (read != sizeof(header)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (memcmp(header, ELF_MAGIC, sizeof(ELF_MAGIC)) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t elf_class = header[4]; // e_ident[EI_CLASS]
|
||||
uint8_t data = header[5]; // e_ident[EI_DATA]
|
||||
uint16_t type = read_le16(header + 16); // e_type
|
||||
uint16_t machine = read_le16(header + 18); // e_machine
|
||||
|
||||
return elf_class == requirements->elf_class
|
||||
&& data == requirements->data
|
||||
&& type == requirements->type
|
||||
&& machine == requirements->machine;
|
||||
}
|
||||
273
Modules/app-module/source/exec.cpp
Normal file
273
Modules/app-module/source/exec.cpp
Normal file
@ -0,0 +1,273 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app/exec.h>
|
||||
|
||||
#include <app/private/exec_internal.h>
|
||||
|
||||
#include <app/loader.h>
|
||||
#include <app/location.h>
|
||||
#include <app/manager.h>
|
||||
#include <app/manifest.h>
|
||||
|
||||
#include <service/instance.h>
|
||||
#include <service/manager.h>
|
||||
|
||||
#include <tactility/concurrent/mutex.h>
|
||||
#include <tactility/filesystem/file_system.h>
|
||||
#include <tactility/log.h>
|
||||
#include <tactility/paths.h>
|
||||
|
||||
#include <climits>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unistd.h>
|
||||
#include <unordered_map>
|
||||
|
||||
constexpr auto* TAG = "app_exec";
|
||||
|
||||
namespace {
|
||||
|
||||
struct ExecPathRegistry {
|
||||
std::string paths[APP_EXEC_MAX_PATHS];
|
||||
size_t count = 0;
|
||||
Mutex mutex {};
|
||||
|
||||
ExecPathRegistry() { mutex_construct(&mutex); }
|
||||
};
|
||||
|
||||
ExecPathRegistry& registry() {
|
||||
static ExecPathRegistry instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
// POSIX works with relative paths. The rest of the OS uses anchors relative paths.
|
||||
// Resolving through getcwd() keeps every path callers pass in comparable, regardless of which convention they used.
|
||||
// A path already starting with '/' passes through unchanged, so this is a no-op on ESP32.
|
||||
std::string make_absolute(const std::string& path) {
|
||||
if (path.empty() || path.front() == '/') {
|
||||
return path;
|
||||
}
|
||||
char cwd[PATH_MAX];
|
||||
if (getcwd(cwd, sizeof(cwd)) == nullptr) {
|
||||
return path; // best effort: falls back to today's (broken but not worse) behavior
|
||||
}
|
||||
return std::string(cwd) + "/" + path;
|
||||
}
|
||||
|
||||
// Resolves to absolute (see make_absolute()) and strips a trailing '/' so "/data/bin/" and
|
||||
// "/data/bin" register/compare identically.
|
||||
std::string normalize(const char* path) {
|
||||
std::string result = make_absolute(path);
|
||||
while (result.size() > 1 && result.back() == '/') {
|
||||
result.pop_back();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool has_dotdot_segment(const std::string& path) {
|
||||
size_t start = 0;
|
||||
while (start <= path.size()) {
|
||||
size_t slash = path.find('/', start);
|
||||
size_t length = (slash == std::string::npos ? path.size() : slash) - start;
|
||||
if (path.compare(start, length, "..") == 0) {
|
||||
return true;
|
||||
}
|
||||
if (slash == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
start = slash + 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// True when @a file_path lives strictly under @a dir (a direct or nested child). Never equal to
|
||||
// @a dir itself: a bare directory is never itself an executable candidate.
|
||||
bool is_under_directory(const std::string& file_path, const std::string& dir) {
|
||||
if (file_path.size() <= dir.size() || file_path.compare(0, dir.size(), dir) != 0) {
|
||||
return false;
|
||||
}
|
||||
return file_path[dir.size()] == '/';
|
||||
}
|
||||
|
||||
const AppLoaderApi* find_path_loader_api() {
|
||||
ServiceInstance* instance = service_manager_find_instance(APP_LOADER_PATH_SERVICE_ID);
|
||||
if (instance == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return static_cast<const AppLoaderApi*>(service_instance_get_data(instance));
|
||||
}
|
||||
|
||||
// region Ad hoc "run this path directly" registry: owns the AppManifest (and its id/name/path
|
||||
// strings) that app_manager's own ledger only keeps a non-owning pointer to (see
|
||||
// app_manager_add()'s contract; install.cpp's InstallRegistry exists for the same reason).
|
||||
// Keyed by path so re-running the same file reuses its manifest instead of re-adding it.
|
||||
|
||||
struct RunRecord {
|
||||
std::string id;
|
||||
std::string name;
|
||||
std::string path;
|
||||
AppManifest manifest {};
|
||||
};
|
||||
|
||||
struct RunRegistry {
|
||||
std::unordered_map<std::string, std::unique_ptr<RunRecord>> apps;
|
||||
Mutex mutex {};
|
||||
|
||||
RunRegistry() { mutex_construct(&mutex); }
|
||||
};
|
||||
|
||||
RunRegistry& run_registry() {
|
||||
static RunRegistry registry;
|
||||
return registry;
|
||||
}
|
||||
|
||||
std::string last_path_segment(const std::string& path) {
|
||||
auto index = path.find_last_of('/');
|
||||
return index == std::string::npos ? path : path.substr(index + 1);
|
||||
}
|
||||
|
||||
// A short, stable-per-path id that fits AppManifest::id's APP_ID_LENGTH limit regardless of how
|
||||
// long @a path is. FNV-1a's collision odds are irrelevant at the scale of "distinct executables
|
||||
// one user runs from the Files app".
|
||||
std::string make_run_id(const std::string& path) {
|
||||
uint32_t hash = 2166136261u;
|
||||
for (unsigned char c: path) {
|
||||
hash ^= c;
|
||||
hash *= 16777619u;
|
||||
}
|
||||
char id[16];
|
||||
std::snprintf(id, sizeof(id), "run.%08x", static_cast<unsigned int>(hash));
|
||||
return id;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
error_t app_exec_path_add(const char* path) {
|
||||
auto normalized = normalize(path);
|
||||
|
||||
auto& reg = registry();
|
||||
mutex_lock(®.mutex);
|
||||
|
||||
for (size_t i = 0; i < reg.count; i++) {
|
||||
if (reg.paths[i] == normalized) {
|
||||
mutex_unlock(®.mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
if (reg.count >= APP_EXEC_MAX_PATHS) {
|
||||
mutex_unlock(®.mutex);
|
||||
LOG_E(TAG, "Registry full, can't add %s", path);
|
||||
return ERROR_OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
reg.paths[reg.count++] = normalized;
|
||||
mutex_unlock(®.mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
error_t app_exec_path_remove(const char* path) {
|
||||
auto normalized = normalize(path);
|
||||
|
||||
auto& reg = registry();
|
||||
mutex_lock(®.mutex);
|
||||
|
||||
for (size_t i = 0; i < reg.count; i++) {
|
||||
if (reg.paths[i] == normalized) {
|
||||
reg.paths[i] = reg.paths[reg.count - 1];
|
||||
reg.count--;
|
||||
mutex_unlock(®.mutex);
|
||||
return ERROR_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
mutex_unlock(®.mutex);
|
||||
return ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
bool app_exec_path_allowed(const char* file_path) {
|
||||
std::string path = make_absolute(file_path);
|
||||
if (has_dotdot_segment(path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& reg = registry();
|
||||
mutex_lock(®.mutex);
|
||||
bool allowed = false;
|
||||
for (size_t i = 0; i < reg.count; i++) {
|
||||
if (is_under_directory(path, reg.paths[i])) {
|
||||
allowed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
mutex_unlock(®.mutex);
|
||||
return allowed;
|
||||
}
|
||||
|
||||
bool app_exec_is_executable_path(const char* path) {
|
||||
const AppLoaderApi* loader = find_path_loader_api();
|
||||
if (loader == nullptr || loader->is_executable == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AppLocation location { APP_LOCATION_PATH, const_cast<char*>(path) };
|
||||
return loader->is_executable(location);
|
||||
}
|
||||
|
||||
error_t app_exec_run_path(const char* path, int argc, const char* const argv[], AppInstanceId* out_app_instance_id) {
|
||||
if (!app_exec_is_executable_path(path)) {
|
||||
return ERROR_NOT_ALLOWED;
|
||||
}
|
||||
|
||||
std::string path_str(path);
|
||||
auto& reg = run_registry();
|
||||
mutex_lock(®.mutex);
|
||||
|
||||
auto iterator = reg.apps.find(path_str);
|
||||
std::string id;
|
||||
if (iterator != reg.apps.end()) {
|
||||
id = iterator->second->id;
|
||||
mutex_unlock(®.mutex);
|
||||
} else {
|
||||
auto record = std::make_unique<RunRecord>();
|
||||
record->id = make_run_id(path_str);
|
||||
record->name = last_path_segment(path_str);
|
||||
record->path = path_str;
|
||||
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 = APP_MANIFEST_FLAG_HIDDEN,
|
||||
.stack = { .depth = 0, .desired_memory_capability = 0 },
|
||||
};
|
||||
|
||||
error_t add_result = app_manager_add(&record->manifest);
|
||||
if (add_result != ERROR_NONE) {
|
||||
mutex_unlock(®.mutex);
|
||||
LOG_E(TAG, "Failed to register %s: %s", path, error_to_string(add_result));
|
||||
return add_result;
|
||||
}
|
||||
|
||||
id = record->id;
|
||||
reg.apps[path_str] = std::move(record);
|
||||
mutex_unlock(®.mutex);
|
||||
}
|
||||
|
||||
return app_manager_start_with_parameters(id.c_str(), argc, argv, out_app_instance_id);
|
||||
}
|
||||
|
||||
void app_exec_register_default_paths() {
|
||||
char data_root[192];
|
||||
if (paths_get_data_path(data_root, sizeof(data_root)) == ERROR_NONE) {
|
||||
app_exec_path_add((std::string(data_root) + "/bin").c_str());
|
||||
app_exec_path_add((std::string(data_root) + "/app").c_str());
|
||||
} else {
|
||||
LOG_W(TAG, "Failed to resolve data path, not registering its executable directories");
|
||||
}
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@ -24,10 +24,15 @@ int32_t api_run(AppRuntime runtime, uint32_t /*app_instance_id*/, int argc, char
|
||||
void api_unload(AppRuntime /*unused*/) {
|
||||
}
|
||||
|
||||
bool api_is_executable(AppLocation location) {
|
||||
return location.type == APP_LOCATION_MEMORY && location.location != nullptr;
|
||||
}
|
||||
|
||||
AppLoaderApi memory_loader_api = {
|
||||
.load = api_load,
|
||||
.run = api_run,
|
||||
.unload = api_unload,
|
||||
.is_executable = api_is_executable,
|
||||
};
|
||||
|
||||
void* create_service(const ServiceManifest*) {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app/event.h>
|
||||
#include <app/exec.h>
|
||||
#include <app/install.h>
|
||||
#include <app/io.h>
|
||||
#include <app/manager.h>
|
||||
@ -9,6 +10,8 @@
|
||||
#include <app/scheduler.h>
|
||||
#include <app/stream.h>
|
||||
|
||||
#include <app/private/exec_internal.h>
|
||||
|
||||
#include <service/manager.h>
|
||||
|
||||
#include <tactility/concurrent/task_event_group.h>
|
||||
@ -73,7 +76,12 @@ static const ModuleSymbol SYMBOLS[] = {
|
||||
};
|
||||
|
||||
static error_t start() {
|
||||
return service_manager_add(&app_internal_loader_service_manifest, /*auto_start=*/true);
|
||||
error_t error = service_manager_add(&app_internal_loader_service_manifest, /*auto_start=*/true);
|
||||
if (error != ERROR_NONE) {
|
||||
return error;
|
||||
}
|
||||
app_exec_register_default_paths();
|
||||
return ERROR_NONE;
|
||||
}
|
||||
|
||||
static error_t stop() {
|
||||
|
||||
136
Modules/app-module/tests/source/exec_test.cpp
Normal file
136
Modules/app-module/tests/source/exec_test.cpp
Normal file
@ -0,0 +1,136 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "doctest.h"
|
||||
|
||||
#include <app/exec.h>
|
||||
#include <app/loader.h>
|
||||
#include <app/location.h>
|
||||
|
||||
#include <service/instance.h>
|
||||
#include <service/manager.h>
|
||||
|
||||
#include <climits>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <unistd.h>
|
||||
|
||||
extern ServiceManifest app_internal_loader_service_manifest;
|
||||
|
||||
namespace {
|
||||
|
||||
const AppLoaderApi* memory_loader_api() {
|
||||
if (service_manager_find_instance(APP_LOADER_MEMORY_SERVICE_ID) == nullptr) {
|
||||
service_manager_add(&app_internal_loader_service_manifest, /*auto_start=*/true);
|
||||
}
|
||||
auto* instance = service_manager_find_instance(APP_LOADER_MEMORY_SERVICE_ID);
|
||||
REQUIRE(instance != nullptr);
|
||||
return static_cast<const AppLoaderApi*>(service_instance_get_data(instance));
|
||||
}
|
||||
|
||||
int32_t fake_entry(int, char*[]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("app_exec_path_add/app_exec_path_allowed accept nested children of a registered directory") {
|
||||
REQUIRE_EQ(app_exec_path_add("/exec-test/bin"), ERROR_NONE);
|
||||
|
||||
CHECK(app_exec_path_allowed("/exec-test/bin/foo"));
|
||||
CHECK(app_exec_path_allowed("/exec-test/bin/sub/foo"));
|
||||
CHECK_FALSE(app_exec_path_allowed("/exec-test/binaries/foo"));
|
||||
CHECK_FALSE(app_exec_path_allowed("/exec-test/bin"));
|
||||
|
||||
app_exec_path_remove("/exec-test/bin");
|
||||
}
|
||||
|
||||
TEST_CASE("app_exec_path_allowed rejects a '..' segment even under a registered directory") {
|
||||
REQUIRE_EQ(app_exec_path_add("/exec-test/bin2"), ERROR_NONE);
|
||||
|
||||
CHECK_FALSE(app_exec_path_allowed("/exec-test/bin2/../etc/passwd"));
|
||||
|
||||
app_exec_path_remove("/exec-test/bin2");
|
||||
}
|
||||
|
||||
TEST_CASE("app_exec_path_add is a no-op for a path already registered") {
|
||||
REQUIRE_EQ(app_exec_path_add("/exec-test/dup"), ERROR_NONE);
|
||||
REQUIRE_EQ(app_exec_path_add("/exec-test/dup"), ERROR_NONE);
|
||||
|
||||
CHECK(app_exec_path_allowed("/exec-test/dup/foo"));
|
||||
|
||||
app_exec_path_remove("/exec-test/dup");
|
||||
}
|
||||
|
||||
TEST_CASE("app_exec_path_add does not register same item twice") {
|
||||
REQUIRE_EQ(app_exec_path_add("/exec-test/dup"), ERROR_NONE);
|
||||
REQUIRE_EQ(app_exec_path_add("/exec-test/dup"), ERROR_NONE);
|
||||
app_exec_path_remove("/exec-test/dup");
|
||||
CHECK(!app_exec_path_allowed("/exec-test/dup/foo"));
|
||||
}
|
||||
|
||||
TEST_CASE("app_exec_path_remove reports ERROR_NOT_FOUND for a path that was never registered") {
|
||||
CHECK_EQ(app_exec_path_remove("/exec-test/never-registered"), ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
TEST_CASE("app_exec_path_remove makes a previously allowed path disallowed again") {
|
||||
REQUIRE_EQ(app_exec_path_add("/exec-test/removable"), ERROR_NONE);
|
||||
REQUIRE(app_exec_path_allowed("/exec-test/removable/foo"));
|
||||
|
||||
REQUIRE_EQ(app_exec_path_remove("/exec-test/removable"), ERROR_NONE);
|
||||
CHECK_FALSE(app_exec_path_allowed("/exec-test/removable/foo"));
|
||||
}
|
||||
|
||||
TEST_CASE("app_exec_path_add resolves a relative path to absolute, so it matches an absolute incoming path") {
|
||||
char cwd[PATH_MAX];
|
||||
REQUIRE(getcwd(cwd, sizeof(cwd)) != nullptr);
|
||||
|
||||
REQUIRE_EQ(app_exec_path_add("exec-test-relative/bin"), ERROR_NONE);
|
||||
|
||||
std::string absolute_child = std::string(cwd) + "/exec-test-relative/bin/foo";
|
||||
CHECK(app_exec_path_allowed(absolute_child.c_str()));
|
||||
|
||||
app_exec_path_remove("exec-test-relative/bin");
|
||||
CHECK_FALSE(app_exec_path_allowed(absolute_child.c_str()));
|
||||
}
|
||||
|
||||
TEST_CASE("app_exec_path_allowed resolves a relative incoming path to absolute, so it matches an absolute registered directory") {
|
||||
char cwd[PATH_MAX];
|
||||
REQUIRE(getcwd(cwd, sizeof(cwd)) != nullptr);
|
||||
|
||||
std::string absolute_dir = std::string(cwd) + "/exec-test-relative2/bin";
|
||||
REQUIRE_EQ(app_exec_path_add(absolute_dir.c_str()), ERROR_NONE);
|
||||
|
||||
CHECK(app_exec_path_allowed("exec-test-relative2/bin/foo"));
|
||||
|
||||
app_exec_path_remove(absolute_dir.c_str());
|
||||
}
|
||||
|
||||
TEST_CASE("app_exec_path_add returns ERROR_OUT_OF_MEMORY once the registry is full") {
|
||||
char path[32];
|
||||
int added = 0;
|
||||
for (added = 0; added < APP_EXEC_MAX_PATHS + 1; added++) {
|
||||
snprintf(path, sizeof(path), "/exec-test/overflow%d", added);
|
||||
if (app_exec_path_add(path) != ERROR_NONE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
CHECK_LT(added, APP_EXEC_MAX_PATHS + 1);
|
||||
|
||||
for (int i = 0; i < added; i++) {
|
||||
snprintf(path, sizeof(path), "/exec-test/overflow%d", i);
|
||||
app_exec_path_remove(path);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("memory loader's is_executable() reports a non-null AppLocation as executable, a null one as not") {
|
||||
const AppLoaderApi* loader = memory_loader_api();
|
||||
REQUIRE(loader->is_executable != nullptr);
|
||||
|
||||
AppLocation runnable { APP_LOCATION_MEMORY, reinterpret_cast<void*>(fake_entry) };
|
||||
CHECK(loader->is_executable(runnable));
|
||||
|
||||
AppLocation empty { APP_LOCATION_MEMORY, nullptr };
|
||||
CHECK_FALSE(loader->is_executable(empty));
|
||||
|
||||
AppLocation wrong_type { APP_LOCATION_PATH, reinterpret_cast<void*>(fake_entry) };
|
||||
CHECK_FALSE(loader->is_executable(wrong_type));
|
||||
}
|
||||
@ -1,4 +1,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include <app/elf_check.h>
|
||||
#include <app/exec.h>
|
||||
#include <app/loader.h>
|
||||
#include <app/location.h>
|
||||
|
||||
@ -27,6 +29,36 @@ bool is_regular_file(const std::string& path) {
|
||||
return ::stat(path.c_str(), &path_stat) == 0 && S_ISREG(path_stat.st_mode);
|
||||
}
|
||||
|
||||
#ifndef __APPLE__
|
||||
constexpr ElfRequirements EXECUTABLE_REQUIREMENTS = {
|
||||
.elf_class = ELF_CLASS_64,
|
||||
.data = ELF_DATA_2LSB,
|
||||
.type = ELF_TYPE_DYN,
|
||||
#if defined(__x86_64__)
|
||||
.machine = ELF_MACHINE_X86_64,
|
||||
#elif defined(__aarch64__)
|
||||
.machine = ELF_MACHINE_AARCH64,
|
||||
#else
|
||||
#error "Unsupported POSIX architecture for ELF machine check"
|
||||
#endif
|
||||
};
|
||||
#endif
|
||||
|
||||
// Validates an already-resolved binary path (see resolve_app_path()) before it's handed to
|
||||
// dlopen(): the extension and directory checks are cheap string comparisons, so the file is only
|
||||
// opened as a last resort.
|
||||
bool is_executable_file(const std::string& resolved_path) {
|
||||
if (!resolved_path.ends_with(".so") || !app_exec_path_allowed(resolved_path.c_str())) {
|
||||
return false;
|
||||
}
|
||||
#ifdef __APPLE__
|
||||
// The simulator's app binaries are Mach-O on macOS, not ELF, so there is no header to check.
|
||||
return is_regular_file(resolved_path);
|
||||
#else
|
||||
return elf_check_file(resolved_path.c_str(), &EXECUTABLE_REQUIREMENTS);
|
||||
#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().
|
||||
@ -56,6 +88,11 @@ error_t api_load(AppLocation location, AppRuntime* out_runtime) {
|
||||
return error;
|
||||
}
|
||||
|
||||
if (!is_executable_file(app_path)) {
|
||||
LOG_E(TAG, "Not executable: %s", app_path.c_str());
|
||||
return ERROR_NOT_ALLOWED;
|
||||
}
|
||||
|
||||
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
|
||||
@ -100,10 +137,24 @@ void api_unload(AppRuntime runtime_ptr) {
|
||||
delete runtime;
|
||||
}
|
||||
|
||||
bool api_is_executable(AppLocation location) {
|
||||
if (location.type != APP_LOCATION_PATH) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string app_path;
|
||||
if (resolve_app_path(static_cast<const char*>(location.location), app_path) != ERROR_NONE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return is_executable_file(app_path);
|
||||
}
|
||||
|
||||
AppLoaderApi loader_api = {
|
||||
.load = api_load,
|
||||
.run = api_run,
|
||||
.unload = api_unload,
|
||||
.is_executable = api_is_executable,
|
||||
};
|
||||
|
||||
void* create_service(const ServiceManifest*) {
|
||||
|
||||
@ -11,6 +11,10 @@ add_library(app_posix_module_test_fixture SHARED EXCLUDE_FROM_ALL ${CMAKE_CURREN
|
||||
target_include_directories(app_posix_module_test_fixture PRIVATE ${CMAKE_SOURCE_DIR}/Modules/app-module/include)
|
||||
set_target_properties(app_posix_module_test_fixture PROPERTIES POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
# A file with a ".so" extension but no ELF header, for is_executable() rejection tests.
|
||||
set(NON_ELF_FIXTURE_PATH "${CMAKE_CURRENT_BINARY_DIR}/not-elf.so")
|
||||
file(WRITE "${NON_ELF_FIXTURE_PATH}" "not an elf file")
|
||||
|
||||
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)
|
||||
@ -18,6 +22,7 @@ add_dependencies(AppPosixModuleTests app_posix_module_test_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}"
|
||||
)
|
||||
|
||||
add_test(NAME AppPosixModuleTests COMMAND AppPosixModuleTests)
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
#include "doctest.h"
|
||||
|
||||
#include <app/event.h>
|
||||
#include <app/exec.h>
|
||||
#include <app/loader.h>
|
||||
#include <app/manager.h>
|
||||
#include <app/scheduler.h>
|
||||
@ -11,6 +12,7 @@
|
||||
#include <tactility/delay.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
|
||||
extern ServiceManifest loader_service_manifest; // app-posix-module's own
|
||||
extern ServiceManifest app_internal_loader_service_manifest; // app-module's real memory loader
|
||||
@ -29,6 +31,13 @@ void ensure_memory_loader_registered() {
|
||||
}
|
||||
}
|
||||
|
||||
std::string directory_of(const std::string& path) {
|
||||
auto slash = path.find_last_of('/');
|
||||
return slash == std::string::npos ? "." : path.substr(0, slash);
|
||||
}
|
||||
|
||||
const std::string FIXTURE_DIR = directory_of(FIXTURE_APP_PATH);
|
||||
|
||||
bool wait_for_state(AppInstanceId id, AppInstanceState target, uint32_t timeout_ms) {
|
||||
uint32_t waited = 0;
|
||||
while (waited < timeout_ms) {
|
||||
@ -93,6 +102,7 @@ int32_t parent_app_main(int, char*[]) {
|
||||
TEST_CASE("app-posix-module's loader-path service dlopen()s a .so and calls its main(), which resolves a real Tactility symbol against the host") {
|
||||
ensure_path_loader_registered();
|
||||
ensure_memory_loader_registered();
|
||||
REQUIRE_EQ(app_exec_path_add(FIXTURE_DIR.c_str()), ERROR_NONE);
|
||||
g_fixture_result.store(-1, std::memory_order_relaxed);
|
||||
g_fixture_result_received.store(false, std::memory_order_relaxed);
|
||||
|
||||
@ -111,3 +121,45 @@ TEST_CASE("app-posix-module's loader-path service dlopen()s a .so and calls its
|
||||
|
||||
app_manager_remove("test.posix.parent");
|
||||
}
|
||||
|
||||
TEST_CASE("app_exec_is_executable_path() accepts a real .so in a registered directory") {
|
||||
ensure_path_loader_registered();
|
||||
REQUIRE_EQ(app_exec_path_add(FIXTURE_DIR.c_str()), ERROR_NONE);
|
||||
|
||||
CHECK(app_exec_is_executable_path(FIXTURE_APP_PATH));
|
||||
}
|
||||
|
||||
TEST_CASE("app_exec_is_executable_path() rejects the same .so when its directory isn't registered") {
|
||||
ensure_path_loader_registered();
|
||||
app_exec_path_remove(FIXTURE_DIR.c_str());
|
||||
|
||||
CHECK_FALSE(app_exec_is_executable_path(FIXTURE_APP_PATH));
|
||||
|
||||
// Restore for any later test case that relies on it being registered.
|
||||
app_exec_path_add(FIXTURE_DIR.c_str());
|
||||
}
|
||||
|
||||
TEST_CASE("app_exec_is_executable_path() rejects a .so-named file with no ELF header") {
|
||||
ensure_path_loader_registered();
|
||||
auto non_elf_dir = directory_of(FIXTURE_NON_ELF_PATH);
|
||||
REQUIRE_EQ(app_exec_path_add(non_elf_dir.c_str()), ERROR_NONE);
|
||||
|
||||
CHECK_FALSE(app_exec_is_executable_path(FIXTURE_NON_ELF_PATH));
|
||||
|
||||
app_exec_path_remove(non_elf_dir.c_str());
|
||||
}
|
||||
|
||||
TEST_CASE("app_exec_is_executable_path() rejects a nonexistent path in a registered directory") {
|
||||
ensure_path_loader_registered();
|
||||
REQUIRE_EQ(app_exec_path_add(FIXTURE_DIR.c_str()), ERROR_NONE);
|
||||
|
||||
CHECK_FALSE(app_exec_is_executable_path((FIXTURE_DIR + "/does-not-exist.so").c_str()));
|
||||
}
|
||||
|
||||
TEST_CASE("app_exec_is_executable_path() rejects an install-directory-shaped path missing its per-arch .so") {
|
||||
ensure_path_loader_registered();
|
||||
REQUIRE_EQ(app_exec_path_add(FIXTURE_DIR.c_str()), ERROR_NONE);
|
||||
|
||||
// FIXTURE_DIR itself has no elf/posix-<arch>.so under it, so resolution fails.
|
||||
CHECK_FALSE(app_exec_is_executable_path(FIXTURE_DIR.c_str()));
|
||||
}
|
||||
|
||||
@ -30,8 +30,10 @@ class View final {
|
||||
void showActionsForDirectory();
|
||||
void showActionsForFile();
|
||||
void showActionsForMountPoint();
|
||||
void addCommonFileActions();
|
||||
|
||||
void viewFile(const std::string&path, const std::string&filename);
|
||||
void runFile(const std::string& file_path);
|
||||
void createDirEntryWidget(lv_obj_t* parent, dirent& dir_entry);
|
||||
void onNavigate();
|
||||
|
||||
@ -54,6 +56,7 @@ public:
|
||||
void onCutPressed();
|
||||
void onPastePressed();
|
||||
void onEjectPressed();
|
||||
void onRunPressed();
|
||||
void onDirEntryListScrollBegin();
|
||||
void onResult(uint32_t launchId, int32_t result);
|
||||
void deinit();
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
#include <app/install.h>
|
||||
#include <app/event.h>
|
||||
#include <app/exec.h>
|
||||
#include <app/install.h>
|
||||
|
||||
#include <lvgl/lvgl.h>
|
||||
#include <lvgl/widgets/toolbar.h>
|
||||
@ -102,6 +103,11 @@ static void onPastePressedCallback(lv_event_t* event) {
|
||||
view->onPastePressed();
|
||||
}
|
||||
|
||||
static void onRunPressedCallback(lv_event_t* event) {
|
||||
auto* view = static_cast<View*>(lv_event_get_user_data(event));
|
||||
view->onRunPressed();
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region File helpers
|
||||
@ -192,6 +198,8 @@ void View::viewFile(const std::string& path, const std::string& filename) {
|
||||
// Remove forward slash, because we need a relative path
|
||||
notes::start(file_path.substr(1));
|
||||
}
|
||||
} else if (app_exec_is_executable_path(file_path.c_str())) {
|
||||
runFile(file_path);
|
||||
} else {
|
||||
LOG_W(TAG, "Opening files of this type is not supported");
|
||||
}
|
||||
@ -199,6 +207,15 @@ void View::viewFile(const std::string& path, const std::string& filename) {
|
||||
onNavigate();
|
||||
}
|
||||
|
||||
void View::runFile(const std::string& file_path) {
|
||||
LOG_I(TAG, "Running %s", file_path.c_str());
|
||||
AppInstanceId instance_id = 0;
|
||||
if (app_exec_run_path(file_path.c_str(), 0, nullptr, &instance_id) != ERROR_NONE) {
|
||||
LOG_W(TAG, "Failed to run %s", file_path.c_str());
|
||||
alertdialog::start(appInstanceId, "Run failed", "Could not run \"" + file::getLastPathSegment(file_path) + "\".");
|
||||
}
|
||||
}
|
||||
|
||||
bool View::resolveDirentFromListIndex(int32_t list_index, dirent& out_entry) {
|
||||
const bool is_root = (state->getCurrentPath() == "/");
|
||||
const bool has_back = (!is_root && current_start_index > 0);
|
||||
@ -287,6 +304,8 @@ void View::createDirEntryWidget(lv_obj_t* list, dirent& dir_entry) {
|
||||
symbol = LV_SYMBOL_IMAGE;
|
||||
} else if (dir_entry.d_type == file::TT_DT_LNK) {
|
||||
symbol = LV_SYMBOL_LOOP;
|
||||
} else if (app_exec_is_executable_path(file::getChildPath(state->getCurrentPath(), dir_entry.d_name).c_str())) {
|
||||
symbol = LV_SYMBOL_PLAY;
|
||||
} else {
|
||||
symbol = LV_SYMBOL_FILE;
|
||||
}
|
||||
@ -370,9 +389,7 @@ void View::onNewFolderPressed() {
|
||||
inputdialog::start(appInstanceId, "New Folder", "Enter folder name:", "");
|
||||
}
|
||||
|
||||
void View::showActions() {
|
||||
lv_obj_clean(action_list);
|
||||
|
||||
void View::addCommonFileActions() {
|
||||
auto* copy_button = lv_list_add_button(action_list, LV_SYMBOL_COPY, "Copy");
|
||||
lv_obj_add_event_cb(copy_button, onCopyPressedCallback, LV_EVENT_SHORT_CLICKED, this);
|
||||
auto* cut_button = lv_list_add_button(action_list, LV_SYMBOL_CUT, "Cut");
|
||||
@ -381,12 +398,27 @@ void View::showActions() {
|
||||
lv_obj_add_event_cb(rename_button, onRenamePressedCallback, LV_EVENT_SHORT_CLICKED, this);
|
||||
auto* delete_button = lv_list_add_button(action_list, LV_SYMBOL_TRASH, "Delete");
|
||||
lv_obj_add_event_cb(delete_button, onDeletePressedCallback, LV_EVENT_SHORT_CLICKED, this);
|
||||
}
|
||||
|
||||
void View::showActions() {
|
||||
lv_obj_clean(action_list);
|
||||
addCommonFileActions();
|
||||
lv_obj_remove_flag(action_list, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
|
||||
void View::showActionsForDirectory() { showActions(); }
|
||||
void View::showActionsForFile() { showActions(); }
|
||||
|
||||
void View::showActionsForFile() {
|
||||
lv_obj_clean(action_list);
|
||||
|
||||
if (app_exec_is_executable_path(state->getSelectedChildPath().c_str())) {
|
||||
auto* run_button = lv_list_add_button(action_list, LV_SYMBOL_PLAY, "Run");
|
||||
lv_obj_add_event_cb(run_button, onRunPressedCallback, LV_EVENT_SHORT_CLICKED, this);
|
||||
}
|
||||
|
||||
addCommonFileActions();
|
||||
lv_obj_remove_flag(action_list, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
|
||||
void View::showActionsForMountPoint() {
|
||||
lv_obj_clean(action_list);
|
||||
@ -397,6 +429,12 @@ void View::showActionsForMountPoint() {
|
||||
lv_obj_remove_flag(action_list, LV_OBJ_FLAG_HIDDEN);
|
||||
}
|
||||
|
||||
void View::onRunPressed() {
|
||||
std::string file_path = state->getSelectedChildPath();
|
||||
onNavigate();
|
||||
runFile(file_path);
|
||||
}
|
||||
|
||||
void View::onEjectPressed() {
|
||||
std::string mount_path = state->getSelectedChildPath();
|
||||
LOG_I(TAG, "Ejecting %s", mount_path.c_str());
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user