refactor: decompose main.c and payload_mgr.c into focused modules

Split two monolithic files (main.c 1538→233 lines, payload_mgr.c
2355→467 lines) into 8 single-responsibility modules:

- http_server.c: MHD request router and all route handlers
- repository.c: repo cache, download, install/commit, self-update
- sources.c: multi-source repository management
- config.c: PldmgrConfig struct, read/write, JSON config handler
- log_server.c: ring buffer, SSE streaming, pldmgr_log()
- json_helpers.c: JsonListBuilder, JSON parser, file I/O utilities
- sha256.c: SHA256 hash implementation
- process_mgr.c: process listing and kill

Introduces PldmgrConfig struct to replace the 8-out-param
read_next_config_values() pattern. Renames extracted functions to
match their new modules (e.g. repository_install_download,
sources_add, process_kill). No HTTP route changes — frontend
is unaffected.
This commit is contained in:
najdek committed 2026-06-03 18:22:26 +02:00
1 parent 631e7de0c6
commit 2189ee962d
21 files changed
+3919 -3811

No files matched your search

+5 -1
View File
@@ -12,7 +12,11 @@ INCLUDES := -Iinclude -I$(TARGET)/include
LIBS := -L$(TARGET)/lib -lcurl -lmbedtls -lmbedx509 -lmbedcrypto -lmicrohttpd -lpthread -lSceNetCtl -lSceUserService -lSceSystemService -lSceAppInstUtil -lSceHttp2 -lSceSsl -lSceNet
# Source Files
SRCS := src/main.c src/payload_mgr.c src/ps5_launcher.c src/notification.c src/utils.c src/autoload.c src/app_installer.c
SRCS := src/main.c src/http_server.c src/config.c src/log_server.c \
src/sha256.c src/json_helpers.c src/repository.c src/sources.c \
src/payload_mgr.c src/process_mgr.c \
src/ps5_launcher.c src/notification.c src/utils.c src/autoload.c \
src/app_installer.c
OBJS := $(SRCS:.c=.o)
ELF := pldmgr.elf
+48
View File
@@ -0,0 +1,48 @@
#ifndef CONFIG_H
#define CONFIG_H
#include <stddef.h>
/* Consolidated configuration state for pldmgr_config.txt */
typedef struct PldmgrConfig {
int autoload_enabled; /* Default: 0 (off) */
long last_repository_update; /* Default: 0 */
int auto_browser_open; /* Default: 1 (on) */
int autoload_delay; /* Default: 5 (seconds) */
int kill_disc_player; /* Default: 1 (on) */
int scan_usb_payloads; /* Default: 0 (off) */
int auto_install_app; /* Default: 1 (on) */
int multi_sources_enabled; /* Default: 0 (off) */
} PldmgrConfig;
/* Read all config values from PLDMGR_CONFIG_PATH.
* Missing file / keys → defaults above. */
void config_read(PldmgrConfig *cfg);
/* Write all config values to PLDMGR_CONFIG_PATH.
* Returns 0 on success, -1 on failure. */
int config_write(const PldmgrConfig *cfg);
/* Read a single boolean (0/1) key from the config file.
* Returns default_val if the key or file is missing. */
int config_read_bool(const char *key, int default_val);
/* Apply a JSON config update sent from the frontend /set_config route.
* Handles AUTOLOAD_ENABLED, AUTO_BROWSER_OPEN, AUTOLOAD_DELAY,
* KILL_DISC_PLAYER_ON_STARTUP, SCAN_USB_PAYLOADS, AUTO_INSTALL_APP,
* MULTI_SOURCES_ENABLED, and AUTOLOAD_LIST.
* Returns 0 on success, -1 on error. */
int config_handle_set_json(const char *json_data);
/* Upsert a single key=value line in the config file.
* Returns 0 on success, -1 on failure. */
int config_upsert_value(const char *key, const char *value);
/* Read the LAST_REPOSITORY_UPDATE timestamp from the config file.
* Always succeeds; sets *out_ts to 0 if not found. */
int config_read_last_update(long *out_ts);
/* Write the LAST_REPOSITORY_UPDATE timestamp to the config file. */
int config_write_last_update(long ts);
#endif
+12
View File
@@ -0,0 +1,12 @@
#ifndef HTTP_SERVER_H
#define HTTP_SERVER_H
#include <microhttpd.h>
/* MHD request handler callback — dispatches all routes. */
enum MHD_Result http_on_request(void *cls, struct MHD_Connection *conn,
const char *url, const char *method,
const char *version, const char *upload_data,
size_t *upload_data_size, void **con_cls);
#endif
+35
View File
@@ -0,0 +1,35 @@
#ifndef JSON_HELPERS_H
#define JSON_HELPERS_H
#include <stddef.h>
/* Incremental JSON array/object builder */
typedef struct JsonListBuilder {
char *buf;
size_t size;
size_t pos;
int first;
} JsonListBuilder;
/* Append formatted text to the builder. Returns 0 on success, -1 on overflow. */
int json_append(JsonListBuilder *jb, const char *fmt, ...);
/* Extract a quoted string value for a given key from a JSON object bounded by
* [obj_start, obj_end). Returns 0 on success, -1 if key not found. */
int json_extract_string(const char *obj_start, const char *obj_end,
const char *key, char *out, size_t out_size);
/* Read an entire file into a malloc'd buffer. Caller must free *out_buf.
* Returns 0 on success, -1 on failure. */
int read_file_text(const char *path, char **out_buf, size_t *out_size);
/* Write data to a file atomically. Returns 0 on success, -1 on failure. */
int write_file_text(const char *path, const char *data, size_t size);
/* Create a directory if it doesn't exist. Returns 0 on success. */
int mkdir_if_missing(const char *path);
/* Recursively create all directories in path. Returns 0 on success. */
int ensure_dir_recursive(const char *path);
#endif
+30
View File
@@ -0,0 +1,30 @@
#ifndef LOG_SERVER_H
#define LOG_SERVER_H
#include <stddef.h>
#include <stdint.h>
#include <microhttpd.h>
#define MAX_LOG_LINES 100
#define MAX_LOG_LINE_LEN 256
/* SSE log stream connection state */
struct LogSSEConnection {
int last_log_version;
int sent_initial;
};
/* MHD content reader callback for SSE log streaming */
ssize_t log_stream_callback(void *cls, uint64_t pos, char *buf, size_t max);
/* MHD free callback for SSE connections */
void log_stream_cleanup(void *cls);
/* Build the /log JSON response into buf. Returns bytes written. */
size_t log_build_json(char *buf, size_t buf_size);
/* Track whether any browser client has connected */
void log_server_set_active(void);
/* pldmgr_server_is_active() and pldmgr_log() are declared in pldmgr.h */
#endif
+12 -26
View File
@@ -3,41 +3,27 @@
#include <stddef.h>
/*
* Scans directories and populates json_buf with a JSON array of payloads.
* Returns the length of the string written.
/*
* Scans directories and populates json_buf with a JSON array of payloads.
* Returns the length of the string written.
*/
size_t payload_mgr_list_json(char *json_buf, size_t buf_size);
int payload_mgr_resolve_path(const char *filename, char *out_path, size_t out_size);
int payload_mgr_delete_payload_file(const char *filename);
int payload_mgr_repository_ensure_fresh(int force_refresh);
size_t payload_mgr_repository_list_json(char *json_buf, size_t buf_size, int force_refresh);
int payload_mgr_repository_install_download(const char *filename, const char *install_source_detail, char *msg_buf, size_t msg_buf_size);
int payload_mgr_repository_install_commit(const char *filename, const char *temp_path, const char *install_source, const char *install_source_detail, char *msg_buf, size_t msg_buf_size);
int payload_mgr_import_to_storage(const char *filename, const char *temp_path, const char *install_source, const char *install_source_detail, char *msg_buf, size_t msg_buf_size);
int payload_mgr_import_to_storage(const char *filename, const char *temp_path,
const char *install_source, const char *install_source_detail,
char *msg_buf, size_t msg_buf_size);
int payload_mgr_check_existing(const char *filename, char *out_json, size_t out_size);
int payload_mgr_write_metadata(const char *payload_path, const char *install_source, const char *install_source_detail);
int payload_mgr_repository_push_json(const char *json, size_t len);
int payload_mgr_write_metadata(const char *payload_path, const char *install_source,
const char *install_source_detail);
/* USB Import */
int payload_mgr_usb_check(const char *usb_path, char *out_json, size_t out_size);
int payload_mgr_usb_move(const char *usb_path, int overwrite, char *out_json, size_t out_size);
int payload_mgr_usb_move(const char *usb_path, int overwrite, char *out_json,
size_t out_size);
int payload_mgr_check_self_update(char *out_path, size_t out_size);
/* Sources Management */
int payload_mgr_sources_list_json(char *buf, size_t size);
int payload_mgr_sources_save(const char *json, size_t len);
int payload_mgr_sources_add(const char *url, char *msg_buf, size_t msg_size);
int payload_mgr_sources_remove(int index, char *msg_buf, size_t msg_size);
/* Multi-Source Repository */
size_t payload_mgr_multi_repository_list_json(char *buf, size_t size, int force_refresh);
int payload_mgr_multi_repository_install(const char *filename, const char *source_id, const char *repo_url, char *msg, size_t msg_size);
/* Processes Management */
size_t payload_mgr_process_list_json(char *buf, size_t max_size);
int payload_mgr_process_kill(int pid);
/* Internal utility — exposed for use by repository.c and sources.c */
void payload_mgr_remove_old_files(const char *dir_path, const char *new_filename);
#endif
+1 -2
View File
@@ -46,10 +46,9 @@
"https://itsplk.github.io/ps5-payloads-mirror/payloads.json"
#define REPOSITORY_REFRESH_INTERVAL_SEC 86400
/* Logging */
/* Logging (implementation in log_server.c) */
void pldmgr_log(const char *fmt, ...);
int pldmgr_server_is_active();
int pldmgr_read_config_bool(const char *key, int default_val);
#include "autoload.h"
#include "notification.h"
+13
View File
@@ -0,0 +1,13 @@
#ifndef PROCESS_MGR_H
#define PROCESS_MGR_H
#include <stddef.h>
/* Build a JSON list of all running processes.
* Returns bytes written. */
size_t process_list_json(char *buf, size_t max_size);
/* Kill a process by PID. Returns 0 on success, -1 on failure. */
int process_kill(int pid);
#endif
+69
View File
@@ -0,0 +1,69 @@
#ifndef REPOSITORY_H
#define REPOSITORY_H
#include <stddef.h>
/* Payload entry from a repository JSON */
typedef struct RepoPayload {
char name[128];
char filename[256];
char url[1024];
char source[1024];
char source_direct[1024];
char description[1024];
char last_update[64];
char version[64];
char checksum[65];
char source_name[256];
} RepoPayload;
/* Download a URL to a local file. Returns 0 on success, -1 on failure. */
int download_to_file(const char *url, const char *out_path);
/* Parse a JSON string containing payload objects into a RepoPayload array.
* Caller must free *out_items. Returns 0 on success, -1 on failure. */
int parse_repository_payloads(const char *json, RepoPayload **out_items, size_t *out_count);
/* Write a .json sidecar file with payload metadata.
* Returns 0 on success. */
int write_payload_details_json(const RepoPayload *item, const char *details_path,
const char *install_source, const char *install_source_detail);
/* Write a minimal sidecar file when full RepoPayload info isn't available. */
int write_simple_payload_details_json(const char *filename, const char *details_path,
const char *install_source, const char *install_source_detail);
/* ── Repository cache management ────────────────────────── */
/* Accept a full JSON blob pushed by the browser and replace the cache. */
int repository_push_json(const char *json, size_t len);
/* Ensure the repository cache is up-to-date. If force_refresh, always re-download. */
int repository_ensure_fresh(int force_refresh);
/* Build the /repository_payloads JSON response.
* Returns bytes written. */
size_t repository_list_json(char *json_buf, size_t buf_size, int force_refresh);
/* Download and install a payload from the default repository.
* msg_buf receives a human-readable status message. */
int repository_install_download(const char *filename, const char *install_source_detail,
char *msg_buf, size_t msg_buf_size);
/* Verify a previously-uploaded file against the repository and commit it.
* msg_buf receives a human-readable status message. */
int repository_install_commit(const char *filename, const char *uploaded_temp_path,
const char *install_source, const char *install_source_detail,
char *msg_buf, size_t msg_buf_size);
/* ── Self-update ────────────────────────────────────────── */
/* Read the PLDMGR_VER: signature from an ELF binary.
* Returns 0 on success. */
int get_elf_pldmgr_version(const char *path, char *out_version, size_t out_size);
/* Scan the payload storage for a newer pldmgr.elf.
* If found, writes the path to out_path and returns 0. */
int repository_check_self_update(char *out_path, size_t out_size);
#endif
+21
View File
@@ -0,0 +1,21 @@
#ifndef SHA256_H
#define SHA256_H
#include <stddef.h>
typedef struct SHA256_CTX {
unsigned char data[64];
unsigned int datalen;
unsigned long long bitlen;
unsigned int state[8];
} SHA256_CTX;
void sha256_init(SHA256_CTX *ctx);
void sha256_update(SHA256_CTX *ctx, const unsigned char data[], size_t len);
void sha256_final(SHA256_CTX *ctx, unsigned char hash[]);
/* Compute SHA256 of a file, writing the 64-char hex string to out_hex.
* Returns 0 on success, -1 on failure. */
int compute_sha256_file(const char *path, char out_hex[65]);
#endif
+27
View File
@@ -0,0 +1,27 @@
#ifndef SOURCES_H
#define SOURCES_H
#include <stddef.h>
/* List all configured sources as JSON. */
int sources_list_json(char *buf, size_t size);
/* Replace the sources list from a JSON POST body. */
int sources_save(const char *json, size_t len);
/* Add a new source by URL. On success, msg_buf receives the source name. */
int sources_add(const char *url, char *msg_buf, size_t msg_size);
/* Remove a source by index. Index 0 (default) cannot be removed. */
int sources_remove(int index, char *msg_buf, size_t msg_size);
/* Build multi-source repository JSON (grouped by source).
* Returns bytes written. */
size_t sources_multi_repository_list_json(char *buf, size_t size, int force_refresh);
/* Download and install a payload from a specific source.
* source_id identifies the source; repo_url is used as fallback detail. */
int sources_multi_repository_install(const char *filename, const char *source_id,
const char *repo_url, char *msg, size_t msg_size);
#endif
+249
View File
@@ -0,0 +1,249 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include "config.h"
#include "pldmgr.h"
#include "json_helpers.h"
/* ── Read / Write full config ──────────────────────────────── */
void config_read(PldmgrConfig *cfg) {
FILE *f;
char line[256];
/* Defaults */
cfg->autoload_enabled = 0;
cfg->last_repository_update = 0;
cfg->auto_browser_open = 1;
cfg->autoload_delay = 5;
cfg->kill_disc_player = 1;
cfg->scan_usb_payloads = 0;
cfg->auto_install_app = 1;
cfg->multi_sources_enabled = 0;
f = fopen(PLDMGR_CONFIG_PATH, "r");
if (!f)
return;
while (fgets(line, sizeof(line), f)) {
if (strncmp(line, "AUTOLOAD_ENABLED=", 17) == 0)
cfg->autoload_enabled = atoi(line + 17);
else if (strncmp(line, "LAST_REPOSITORY_UPDATE=", 23) == 0)
cfg->last_repository_update = atol(line + 23);
else if (strncmp(line, "AUTO_BROWSER_OPEN=", 18) == 0)
cfg->auto_browser_open = atoi(line + 18);
else if (strncmp(line, "AUTOLOAD_DELAY=", 15) == 0)
cfg->autoload_delay = atoi(line + 15);
else if (strncmp(line, "KILL_DISC_PLAYER_ON_STARTUP=", 28) == 0)
cfg->kill_disc_player = atoi(line + 28);
else if (strncmp(line, "SCAN_USB_PAYLOADS=", 18) == 0)
cfg->scan_usb_payloads = atoi(line + 18);
else if (strncmp(line, "AUTO_INSTALL_APP=", 17) == 0)
cfg->auto_install_app = atoi(line + 17);
else if (strncmp(line, "MULTI_SOURCES_ENABLED=", 22) == 0)
cfg->multi_sources_enabled = atoi(line + 22);
}
fclose(f);
}
int config_write(const PldmgrConfig *cfg) {
FILE *f;
mkdir(BASE_DATA_DIR, 0777);
f = fopen(PLDMGR_CONFIG_PATH, "w");
if (!f)
return -1;
fprintf(f, "AUTOLOAD_ENABLED=%d\n", cfg->autoload_enabled ? 1 : 0);
fprintf(f, "LAST_REPOSITORY_UPDATE=%ld\n", cfg->last_repository_update);
fprintf(f, "AUTO_BROWSER_OPEN=%d\n", cfg->auto_browser_open ? 1 : 0);
fprintf(f, "AUTOLOAD_DELAY=%d\n", cfg->autoload_delay);
fprintf(f, "KILL_DISC_PLAYER_ON_STARTUP=%d\n", cfg->kill_disc_player ? 1 : 0);
fprintf(f, "SCAN_USB_PAYLOADS=%d\n", cfg->scan_usb_payloads ? 1 : 0);
fprintf(f, "AUTO_INSTALL_APP=%d\n", cfg->auto_install_app ? 1 : 0);
fprintf(f, "MULTI_SOURCES_ENABLED=%d\n", cfg->multi_sources_enabled ? 1 : 0);
fclose(f);
return 0;
}
/* ── Single-key helpers ────────────────────────────────────── */
int config_read_bool(const char *key, int default_val) {
FILE *f = fopen(PLDMGR_CONFIG_PATH, "r");
if (!f) return default_val;
char line[256];
int res = default_val;
size_t key_len = strlen(key);
while (fgets(line, sizeof(line), f)) {
if (strncmp(line, key, key_len) == 0 && line[key_len] == '=') {
res = atoi(line + key_len + 1);
break;
}
}
fclose(f);
return res;
}
int config_upsert_value(const char *key, const char *value) {
FILE *f;
char line[256];
char old_lines[64][256];
int line_count = 0;
int replaced = 0;
size_t key_len = strlen(key);
ensure_dir_recursive(BASE_DATA_DIR);
f = fopen(PLDMGR_CONFIG_PATH, "r");
if (f) {
while (line_count < 64 && fgets(line, sizeof(line), f)) {
if (strncmp(line, key, key_len) == 0 && line[key_len] == '=') {
snprintf(old_lines[line_count], sizeof(old_lines[line_count]),
"%s=%s\n", key, value);
replaced = 1;
} else {
strncpy(old_lines[line_count], line, sizeof(old_lines[line_count]));
old_lines[line_count][sizeof(old_lines[line_count]) - 1] = '\0';
}
line_count++;
}
fclose(f);
}
if (!replaced && line_count < 64) {
snprintf(old_lines[line_count], sizeof(old_lines[line_count]),
"%s=%s\n", key, value);
line_count++;
}
f = fopen(PLDMGR_CONFIG_PATH, "w");
if (!f)
return -1;
for (int i = 0; i < line_count; i++) {
fputs(old_lines[i], f);
}
fclose(f);
return 0;
}
int config_read_last_update(long *out_ts) {
FILE *f;
char line[256];
long ts = 0;
*out_ts = 0;
f = fopen(PLDMGR_CONFIG_PATH, "r");
if (!f)
return 0;
while (fgets(line, sizeof(line), f)) {
if (strncmp(line, "LAST_REPOSITORY_UPDATE=", 23) == 0) {
ts = atol(line + 23);
break;
}
}
fclose(f);
*out_ts = ts;
return 0;
}
int config_write_last_update(long ts) {
char ts_buf[64];
snprintf(ts_buf, sizeof(ts_buf), "%ld", ts);
return config_upsert_value("LAST_REPOSITORY_UPDATE", ts_buf);
}
/* ── JSON config update handler (from /set_config POST body) ── */
/* Helper: extract a JSON boolean value for a given key.
* Returns 1 for true, 0 for false, or unchanged if key not found. */
static int extract_json_bool(const char *json, const char *key, int current) {
char search[64];
snprintf(search, sizeof(search), "\"%s\"", key);
char *pos = strstr(json, search);
if (!pos) return current;
char *val = strchr(pos, ':');
if (!val) return current;
val++;
while (*val == ' ') val++;
if (strncmp(val, "true", 4) == 0) return 1;
if (strncmp(val, "false", 5) == 0) return 0;
return current;
}
static int extract_json_int(const char *json, const char *key, int current) {
char search[64];
snprintf(search, sizeof(search), "\"%s\"", key);
char *pos = strstr(json, search);
if (!pos) return current;
char *val = strchr(pos, ':');
if (!val) return current;
val++;
while (*val == ' ') val++;
return atoi(val);
}
int config_handle_set_json(const char *json_data) {
if (!json_data)
return -1;
/* Read existing config */
PldmgrConfig cfg;
config_read(&cfg);
/* Check if AUTOLOAD_ENABLED is present */
int enabled = extract_json_bool(json_data, "AUTOLOAD_ENABLED", -1);
if (enabled != -1) {
cfg.autoload_enabled = enabled;
cfg.auto_browser_open = extract_json_bool(json_data, "AUTO_BROWSER_OPEN", cfg.auto_browser_open);
cfg.autoload_delay = extract_json_int(json_data, "AUTOLOAD_DELAY", cfg.autoload_delay);
cfg.kill_disc_player = extract_json_bool(json_data, "KILL_DISC_PLAYER_ON_STARTUP", cfg.kill_disc_player);
cfg.scan_usb_payloads = extract_json_bool(json_data, "SCAN_USB_PAYLOADS", cfg.scan_usb_payloads);
cfg.auto_install_app = extract_json_bool(json_data, "AUTO_INSTALL_APP", cfg.auto_install_app);
cfg.multi_sources_enabled = extract_json_bool(json_data, "MULTI_SOURCES_ENABLED", cfg.multi_sources_enabled);
if (config_write(&cfg) == 0) {
pldmgr_log("[PLDMGR] Saved config to %s\n", PLDMGR_CONFIG_PATH);
}
if (enabled == 0)
pldmgr_autoload_abort();
}
/* Handle AUTOLOAD_LIST if present */
char *list_start = strstr(json_data, "\"AUTOLOAD_LIST\"");
if (list_start) {
char *val = strchr(list_start, ':');
if (val) {
val++;
while (*val == ' ' || *val == '\"')
val++;
char *list_end = strchr(val, '\"');
size_t list_len = list_end ? (size_t)(list_end - val) : 0;
char *list_val = malloc(list_len + 1);
if (list_val) {
memcpy(list_val, val, list_len);
list_val[list_len] = '\0';
mkdir(BASE_DATA_DIR, 0777);
FILE *f = fopen(AUTOLOAD_CONFIG_PATH, "w");
if (f) {
char *token = strtok(list_val, ",");
while (token) {
fprintf(f, "%s\n", token);
token = strtok(NULL, ",");
}
fclose(f);
pldmgr_log("[PLDMGR] Saved autoload list to %s\n",
AUTOLOAD_CONFIG_PATH);
}
free(list_val);
}
}
}
return 0;
}
+957
View File
@@ -0,0 +1,957 @@
/*
* HTTP Server — Route dispatch for all API and asset endpoints.
*
* Contains the MHD on_request callback and all route-handling logic
* previously embedded in main.c.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/stat.h>
#include <microhttpd.h>
#include "http_server.h"
#include "pldmgr.h"
#include "log_server.h"
#include "config.h"
#include "payload_mgr.h"
#include "repository.h"
#include "sources.h"
#include "process_mgr.h"
#include "ps5_launcher.h"
#include "assets_index_html.h"
#include "assets_cache_appcache.h"
#include "assets_favicon_svg.h"
#include "assets_icon_png.h"
#define RESPONSE_BUFFER_SIZE 65536
#define CORS_ORIGIN "*"
/* Shared flag — set from main() shutdown route, read by main loop */
volatile int http_keep_running = 1;
/* ── Helpers ───────────────────────────────────────────────── */
static void add_cors_headers(struct MHD_Response *resp) {
MHD_add_response_header(resp, "Access-Control-Allow-Origin", CORS_ORIGIN);
}
static struct MHD_Response *alloc_response_buffer(char **out) {
*out = malloc(RESPONSE_BUFFER_SIZE);
if (*out)
return NULL;
static const char oom[] = "{\"error\":\"Out of memory\"}";
struct MHD_Response *resp = MHD_create_response_from_buffer(
sizeof(oom) - 1, (void *)oom, MHD_RESPMEM_PERSISTENT);
MHD_add_response_header(resp, "Content-Type", "application/json");
add_cors_headers(resp);
return resp;
}
static int has_supported_extension(const char *path) {
const char *ext = strrchr(path, '.');
if (!ext) return 0;
return strcasecmp(ext, ".elf") == 0 || strcasecmp(ext, ".bin") == 0;
}
static int is_safe_filename(const char *name) {
if (!name || name[0] == '\0') return 0;
if (strstr(name, "/") || strstr(name, "..")) return 0;
return 1;
}
static int is_allowed_payload_path(const char *path) {
if (!path || path[0] == '\0') return 0;
if (strstr(path, "..")) return 0;
if (!has_supported_extension(path)) return 0;
for (int i = 0; i < SCAN_DIRS_COUNT; i++) {
size_t len = strlen(SCAN_DIRS[i]);
if (strncmp(path, SCAN_DIRS[i], len) == 0 &&
(path[len] == '/' || path[len] == '\0')) {
return 1;
}
}
/* Allow USB root payloads if SCAN_USB_PAYLOADS is enabled in config */
if (strncmp(path, "/mnt/usb", 8) == 0 &&
path[8] >= '0' && path[8] <= '7' &&
path[9] == '/') {
if (config_read_bool("SCAN_USB_PAYLOADS", 0)) {
return 1;
}
}
return 0;
}
/* ── POST request state ────────────────────────────────────── */
struct PostStatus {
char *data;
size_t size;
int error;
};
struct UploadStatus {
FILE *fp;
size_t total_size;
int error;
char filename[256];
char temp_path[512];
char repo_url[512];
};
/* ── Main request handler ──────────────────────────────────── */
enum MHD_Result http_on_request(void *cls, struct MHD_Connection *conn,
const char *url, const char *method,
const char *version, const char *upload_data,
size_t *upload_data_size, void **con_cls) {
/* Handle CORS Preflight (OPTIONS) */
if (strcmp(method, "OPTIONS") == 0) {
struct MHD_Response *resp =
MHD_create_response_from_buffer(0, NULL, MHD_RESPMEM_PERSISTENT);
add_cors_headers(resp);
MHD_add_response_header(resp, "Access-Control-Allow-Methods",
"GET, POST, OPTIONS");
MHD_add_response_header(resp, "Access-Control-Allow-Headers",
"Content-Type");
enum MHD_Result ret = MHD_queue_response(conn, MHD_HTTP_OK, resp);
MHD_destroy_response(resp);
return ret;
}
/* Set flag that we received a request (meaning browser is open) */
log_server_set_active();
/* ── Initial call for a new request ────────────────────── */
if (*con_cls == NULL) {
if (strncmp(url, ROUTE_UPLOAD, strlen(ROUTE_UPLOAD)) == 0) {
struct UploadStatus *status = malloc(sizeof(struct UploadStatus));
status->fp = NULL;
status->total_size = 0;
status->error = 0;
const char *filename =
MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "filename");
if (filename && !strstr(filename, "/") && !strstr(filename, "..")) {
char path[512];
mkdir(BASE_DATA_DIR, 0777);
mkdir(PAYLOADS_STORAGE_DIR, 0777);
snprintf(path, sizeof(path), "%s/%s.tmp", PAYLOADS_STORAGE_DIR, filename);
strncpy(status->filename, filename, sizeof(status->filename) - 1);
status->filename[sizeof(status->filename) - 1] = '\0';
strncpy(status->temp_path, path, sizeof(status->temp_path) - 1);
status->temp_path[sizeof(status->temp_path) - 1] = '\0';
status->fp = fopen(path, "wb");
if (!status->fp) {
pldmgr_log("[PLDMGR] !!! FAILED to open temp file: %s\n", path);
status->error = 1;
} else {
pldmgr_log("[PLDMGR] Starting upload to temp: %s\n", path);
}
} else {
pldmgr_log("[PLDMGR] !!! Upload failed: Missing filename parameter\n");
status->error = 1;
}
*con_cls = status;
return MHD_YES;
}
if (strcmp(url, ROUTE_SET_CONFIG) == 0 && strcmp(method, "POST") == 0) {
struct PostStatus *status = malloc(sizeof(struct PostStatus));
status->data = NULL;
status->size = 0;
status->error = 0;
*con_cls = status;
return MHD_YES;
}
if (strcmp(url, ROUTE_REPO_PUSH) == 0 && strcmp(method, "POST") == 0) {
struct PostStatus *status = malloc(sizeof(struct PostStatus));
status->data = NULL;
status->size = 0;
status->error = 0;
*con_cls = status;
return MHD_YES;
}
if (strcmp(url, ROUTE_SOURCES_SET) == 0 && strcmp(method, "POST") == 0) {
struct PostStatus *status = malloc(sizeof(struct PostStatus));
status->data = NULL;
status->size = 0;
status->error = 0;
*con_cls = status;
return MHD_YES;
}
if (strncmp(url, ROUTE_REPO_INSTALL_PUSH,
strlen(ROUTE_REPO_INSTALL_PUSH)) == 0 &&
strcmp(method, "POST") == 0) {
struct UploadStatus *status = malloc(sizeof(struct UploadStatus));
status->fp = NULL;
status->total_size = 0;
status->error = 0;
const char *filename =
MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "filename");
if (filename && !strstr(filename, "/") && !strstr(filename, "..")) {
char path[512];
mkdir(BASE_DATA_DIR, 0777);
mkdir(PAYLOADS_STORAGE_DIR, 0777);
snprintf(path, sizeof(path), "%s/%s.part", PAYLOADS_STORAGE_DIR, filename);
strncpy(status->filename, filename, sizeof(status->filename) - 1);
status->filename[sizeof(status->filename) - 1] = '\0';
strncpy(status->temp_path, path, sizeof(status->temp_path) - 1);
status->temp_path[sizeof(status->temp_path) - 1] = '\0';
const char *repo_url = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "repo_url");
if (repo_url) {
strncpy(status->repo_url, repo_url, sizeof(status->repo_url) - 1);
status->repo_url[sizeof(status->repo_url) - 1] = '\0';
} else {
strncpy(status->repo_url, REPOSITORY_SOURCE_URL, sizeof(status->repo_url) - 1);
status->repo_url[sizeof(status->repo_url) - 1] = '\0';
}
status->fp = fopen(path, "wb");
if (!status->fp) {
pldmgr_log("[PLDMGR] !!! Failed to open for install: %s\n", path);
status->error = 1;
} else {
pldmgr_log("[PLDMGR] Installing payload: %s\n", path);
}
} else {
pldmgr_log("[PLDMGR] !!! install_push: invalid filename\n");
status->error = 1;
}
*con_cls = status;
return MHD_YES;
}
*con_cls = (void *)1;
return MHD_YES;
}
/* ── Handle POST data for /set_config ──────────────────── */
if (strcmp(url, ROUTE_SET_CONFIG) == 0 && strcmp(method, "POST") == 0) {
struct PostStatus *status = (struct PostStatus *)*con_cls;
if (*upload_data_size != 0) {
char *new_data = realloc(status->data, status->size + *upload_data_size + 1);
if (!new_data) {
status->error = 1;
} else {
status->data = new_data;
memcpy(status->data + status->size, upload_data, *upload_data_size);
status->size += *upload_data_size;
status->data[status->size] = '\0';
}
*upload_data_size = 0;
return MHD_YES;
} else {
/* Finished receiving JSON */
pldmgr_log("[PLDMGR] Received config update: %s\n",
status->data ? status->data : "(null)");
if (status->data && !status->error) {
config_handle_set_json(status->data);
}
if (status->data)
free(status->data);
free(status);
*con_cls = NULL;
struct MHD_Response *resp = MHD_create_response_from_buffer(
strlen(MSG_OK), (void *)MSG_OK, MHD_RESPMEM_MUST_COPY);
add_cors_headers(resp);
enum MHD_Result ret = MHD_queue_response(conn, MHD_HTTP_OK, resp);
MHD_destroy_response(resp);
return ret;
}
}
/* ── Handle POST data for /repository_push ─────────────── */
if (strcmp(url, ROUTE_REPO_PUSH) == 0 && strcmp(method, "POST") == 0) {
struct PostStatus *status = (struct PostStatus *)*con_cls;
if (*upload_data_size != 0) {
char *nd = realloc(status->data, status->size + *upload_data_size + 1);
if (!nd) {
status->error = 1;
} else {
status->data = nd;
memcpy(status->data + status->size, upload_data, *upload_data_size);
status->size += *upload_data_size;
status->data[status->size] = '\0';
}
*upload_data_size = 0;
return MHD_YES;
} else {
int ok = -1;
if (status->data && !status->error)
ok = repository_push_json(status->data, status->size);
if (status->data)
free(status->data);
free(status);
*con_cls = NULL;
/* Return the current list JSON so the frontend refreshes in one trip */
char *resp_buf;
struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf);
if (oom_resp) {
enum MHD_Result ret2 = MHD_queue_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, oom_resp);
MHD_destroy_response(oom_resp);
return ret2;
}
size_t len = repository_list_json(resp_buf, RESPONSE_BUFFER_SIZE, 0);
struct MHD_Response *resp = MHD_create_response_from_buffer(
len, (void *)resp_buf, MHD_RESPMEM_MUST_FREE);
MHD_add_response_header(resp, "Content-Type", "application/json");
add_cors_headers(resp);
enum MHD_Result ret2 = MHD_queue_response(
conn, ok == 0 ? MHD_HTTP_OK : MHD_HTTP_BAD_REQUEST, resp);
MHD_destroy_response(resp);
return ret2;
}
}
/* ── Handle POST data for /sources_set ─────────────────── */
if (strcmp(url, ROUTE_SOURCES_SET) == 0 && strcmp(method, "POST") == 0) {
struct PostStatus *status = (struct PostStatus *)*con_cls;
if (*upload_data_size != 0) {
char *nd = realloc(status->data, status->size + *upload_data_size + 1);
if (!nd) {
status->error = 1;
} else {
status->data = nd;
memcpy(status->data + status->size, upload_data, *upload_data_size);
status->size += *upload_data_size;
status->data[status->size] = '\0';
}
*upload_data_size = 0;
return MHD_YES;
} else {
int ok = -1;
if (status->data && !status->error)
ok = sources_save(status->data, status->size);
if (status->data)
free(status->data);
free(status);
*con_cls = NULL;
const char *msg = ok == 0 ? MSG_OK : "Failed to save sources";
struct MHD_Response *resp_ss = MHD_create_response_from_buffer(
strlen(msg), (void *)msg, MHD_RESPMEM_MUST_COPY);
add_cors_headers(resp_ss);
enum MHD_Result ret_ss = MHD_queue_response(
conn, ok == 0 ? MHD_HTTP_OK : MHD_HTTP_INTERNAL_SERVER_ERROR, resp_ss);
MHD_destroy_response(resp_ss);
return ret_ss;
}
}
/* ── Chunked upload for /manage:upload ─────────────────── */
if (strncmp(url, ROUTE_UPLOAD, strlen(ROUTE_UPLOAD)) == 0) {
struct UploadStatus *status = (struct UploadStatus *)*con_cls;
if (*upload_data_size != 0) {
if (status->fp && !status->error) {
size_t written = fwrite(upload_data, 1, *upload_data_size, status->fp);
if (written != *upload_data_size) {
pldmgr_log("[PLDMGR] !!! Write error: expected %zu, got %zu\n",
*upload_data_size, written);
status->error = 1;
}
status->total_size += written;
}
*upload_data_size = 0;
return MHD_YES;
} else {
/* Upload finished */
if (status->fp) {
fflush(status->fp);
fclose(status->fp);
status->fp = NULL;
if (!status->error) {
char msg[256];
if (payload_mgr_import_to_storage(status->filename, status->temp_path, "web_upload", "", msg, sizeof(msg)) != 0) {
pldmgr_log("[PLDMGR] !!! Failed to commit upload: %s\n", msg);
status->error = 1;
}
}
}
pldmgr_log("[PLDMGR] Upload finished. Total bytes: %zu, Error: %d\n",
status->total_size, status->error);
int err = status->error;
free(status);
*con_cls = NULL;
const char *msg = err ? "Error during upload\n" : MSG_OK;
struct MHD_Response *resp = MHD_create_response_from_buffer(
strlen(msg), (void *)msg, MHD_RESPMEM_MUST_COPY);
add_cors_headers(resp);
enum MHD_Result ret = MHD_queue_response(
conn, err ? MHD_HTTP_INTERNAL_SERVER_ERROR : MHD_HTTP_OK, resp);
MHD_destroy_response(resp);
return ret;
}
}
/* ── Chunked upload for /repository_install_push ────────── */
if (strncmp(url, ROUTE_REPO_INSTALL_PUSH, strlen(ROUTE_REPO_INSTALL_PUSH)) == 0) {
struct UploadStatus *status = (struct UploadStatus *)*con_cls;
if (*upload_data_size != 0) {
if (status->fp && !status->error) {
size_t written = fwrite(upload_data, 1, *upload_data_size, status->fp);
if (written != *upload_data_size) {
status->error = 1;
}
status->total_size += written;
}
*upload_data_size = 0;
return MHD_YES;
} else {
if (status->fp) {
fflush(status->fp);
fclose(status->fp);
}
int err = status->error;
char msg_buf[1024] = "";
if (!err) {
if (repository_install_commit(
status->filename, status->temp_path, "repository", status->repo_url, msg_buf,
sizeof(msg_buf)) != 0) {
err = 1;
}
}
pldmgr_log("[PLDMGR] Payload install %s (%zu bytes)\n",
err ? "FAILED" : "complete", status->total_size);
free(status);
*con_cls = NULL;
if (err && msg_buf[0] == '\0') {
snprintf(msg_buf, sizeof(msg_buf), "Write error");
}
char json_resp[1024];
snprintf(json_resp, sizeof(json_resp), "{\"ok\":%s,\"message\":\"%s\"}",
err ? "false" : "true", err ? msg_buf : "Installed");
struct MHD_Response *resp2 = MHD_create_response_from_buffer(
strlen(json_resp), (void *)json_resp, MHD_RESPMEM_MUST_COPY);
MHD_add_response_header(resp2, "Content-Type", "application/json");
add_cors_headers(resp2);
enum MHD_Result ret2 = MHD_queue_response(
conn, err ? MHD_HTTP_INTERNAL_SERVER_ERROR : MHD_HTTP_OK, resp2);
MHD_destroy_response(resp2);
return ret2;
}
}
/* ── Log significant requests ──────────────────────────── */
if (strcmp(url, ROUTE_LOG) != 0 && strcmp(url, ROUTE_INDEX) != 0 &&
strcmp(url, ROUTE_INDEX_HTML) != 0 && strcmp(url, "/favicon.svg") != 0 &&
strcmp(url, "/icon.png") != 0) {
pldmgr_log("[PLDMGR] Request: %s %s\n", method, url);
}
struct MHD_Response *resp = NULL;
enum MHD_Result ret;
int http_status = MHD_HTTP_OK;
/* ── USB routes ────────────────────────────────────────── */
if (strcmp(url, ROUTE_USB_MOVE_CHECK) == 0) {
const char *path = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "path");
if (path) {
char *resp_buf;
struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf);
if (oom_resp) {
ret = MHD_queue_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, oom_resp);
MHD_destroy_response(oom_resp);
return ret;
}
int rc = payload_mgr_usb_check(path, resp_buf, RESPONSE_BUFFER_SIZE);
resp = MHD_create_response_from_buffer(strlen(resp_buf), (void *)resp_buf, MHD_RESPMEM_MUST_FREE);
MHD_add_response_header(resp, "Content-Type", "application/json");
add_cors_headers(resp);
ret = MHD_queue_response(conn, rc == 0 ? MHD_HTTP_OK : MHD_HTTP_BAD_REQUEST, resp);
MHD_destroy_response(resp);
return ret;
} else {
const char *err = "{\"error\":\"Missing path\"}";
resp = MHD_create_response_from_buffer(strlen(err), (void *)err, MHD_RESPMEM_MUST_COPY);
MHD_add_response_header(resp, "Content-Type", "application/json");
}
add_cors_headers(resp);
ret = MHD_queue_response(conn, MHD_HTTP_BAD_REQUEST, resp);
MHD_destroy_response(resp);
return ret;
}
if (strcmp(url, ROUTE_USB_MOVE_PERFORM) == 0) {
const char *path = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "path");
const char *overwrite_str = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "overwrite");
int overwrite = (overwrite_str && strcmp(overwrite_str, "true") == 0) ? 1 : 0;
if (path) {
char *resp_buf;
struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf);
if (oom_resp) {
ret = MHD_queue_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, oom_resp);
MHD_destroy_response(oom_resp);
return ret;
}
int rc = payload_mgr_usb_move(path, overwrite, resp_buf, RESPONSE_BUFFER_SIZE);
resp = MHD_create_response_from_buffer(strlen(resp_buf), (void *)resp_buf, MHD_RESPMEM_MUST_FREE);
MHD_add_response_header(resp, "Content-Type", "application/json");
add_cors_headers(resp);
ret = MHD_queue_response(conn, rc == 0 ? MHD_HTTP_OK : MHD_HTTP_BAD_REQUEST, resp);
MHD_destroy_response(resp);
return ret;
} else {
const char *err = "{\"error\":\"Missing path\"}";
resp = MHD_create_response_from_buffer(strlen(err), (void *)err, MHD_RESPMEM_MUST_COPY);
MHD_add_response_header(resp, "Content-Type", "application/json");
}
add_cors_headers(resp);
ret = MHD_queue_response(conn, MHD_HTTP_BAD_REQUEST, resp);
MHD_destroy_response(resp);
return ret;
}
/* ── Static assets ─────────────────────────────────────── */
if (strcmp(url, ROUTE_INDEX) == 0 || strcmp(url, ROUTE_INDEX_HTML) == 0) {
resp = MHD_create_response_from_buffer(assets_index_html_len,
(void *)assets_index_html,
MHD_RESPMEM_PERSISTENT);
MHD_add_response_header(resp, "Content-Type", "text/html");
MHD_add_response_header(resp, "Cache-Control", "no-cache, must-revalidate");
} else if (strcmp(url, ROUTE_CACHE_MANIFEST) == 0) {
resp = MHD_create_response_from_buffer(assets_cache_appcache_len,
(void *)assets_cache_appcache,
MHD_RESPMEM_PERSISTENT);
MHD_add_response_header(resp, "Content-Type", "text/cache-manifest");
MHD_add_response_header(resp, "Cache-Control", "no-cache, must-revalidate");
} else if (strcmp(url, "/favicon.svg") == 0) {
resp = MHD_create_response_from_buffer(assets_favicon_svg_len,
(void *)assets_favicon_svg,
MHD_RESPMEM_PERSISTENT);
MHD_add_response_header(resp, "Content-Type", "image/svg+xml");
MHD_add_response_header(resp, "Cache-Control", "max-age=604800");
} else if (strcmp(url, "/icon.png") == 0) {
resp = MHD_create_response_from_buffer(assets_icon_png_len,
(void *)assets_icon_png,
MHD_RESPMEM_PERSISTENT);
MHD_add_response_header(resp, "Content-Type", "image/png");
MHD_add_response_header(resp, "Cache-Control", "max-age=604800");
/* ── API routes ────────────────────────────────────────── */
} else if (strcmp(url, ROUTE_CHECK) == 0) {
const char *filename = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "filename");
if (!filename) {
const char *err = "{\"error\":\"Missing filename\"}";
resp = MHD_create_response_from_buffer(strlen(err), (void *)err, MHD_RESPMEM_MUST_COPY);
MHD_add_response_header(resp, "Content-Type", "application/json");
add_cors_headers(resp);
return MHD_queue_response(conn, MHD_HTTP_BAD_REQUEST, resp);
}
if (!is_safe_filename(filename)) {
const char *err = "{\"error\":\"Invalid filename\"}";
resp = MHD_create_response_from_buffer(strlen(err), (void *)err, MHD_RESPMEM_MUST_COPY);
MHD_add_response_header(resp, "Content-Type", "application/json");
add_cors_headers(resp);
return MHD_queue_response(conn, MHD_HTTP_BAD_REQUEST, resp);
}
char *resp_buf;
struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf);
if (oom_resp)
return MHD_queue_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, oom_resp);
payload_mgr_check_existing(filename, resp_buf, RESPONSE_BUFFER_SIZE);
resp = MHD_create_response_from_buffer(strlen(resp_buf), (void *)resp_buf, MHD_RESPMEM_MUST_FREE);
MHD_add_response_header(resp, "Content-Type", "application/json");
add_cors_headers(resp);
return MHD_queue_response(conn, MHD_HTTP_OK, resp);
} else if (strcmp(url, ROUTE_LIST_PAYLOADS) == 0) {
char *resp_buf;
struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf);
if (oom_resp)
return MHD_queue_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, oom_resp);
size_t len = payload_mgr_list_json(resp_buf, RESPONSE_BUFFER_SIZE);
resp = MHD_create_response_from_buffer(len, (void *)resp_buf, MHD_RESPMEM_MUST_FREE);
MHD_add_response_header(resp, "Content-Type", "application/json");
} else if (strcmp(url, ROUTE_PROCESSES_LIST) == 0) {
char *resp_buf;
struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf);
if (oom_resp)
return MHD_queue_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, oom_resp);
size_t len = process_list_json(resp_buf, RESPONSE_BUFFER_SIZE);
resp = MHD_create_response_from_buffer(len, (void *)resp_buf, MHD_RESPMEM_MUST_FREE);
MHD_add_response_header(resp, "Content-Type", "application/json");
} else if (strcmp(url, ROUTE_PROCESS_KILL) == 0) {
const char *pid_str = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "pid");
if (!pid_str) {
const char *err = "{\"ok\":false,\"message\":\"Missing pid\"}";
resp = MHD_create_response_from_buffer(strlen(err), (void *)err, MHD_RESPMEM_MUST_COPY);
MHD_add_response_header(resp, "Content-Type", "application/json");
add_cors_headers(resp);
return MHD_queue_response(conn, MHD_HTTP_BAD_REQUEST, resp);
}
int pid = atoi(pid_str);
int rc = process_kill(pid);
char json_resp[256];
snprintf(json_resp, sizeof(json_resp), "{\"ok\":%s,\"message\":\"%s\"}",
rc == 0 ? "true" : "false", rc == 0 ? "Killed" : "Failed to kill");
resp = MHD_create_response_from_buffer(strlen(json_resp), (void *)json_resp, MHD_RESPMEM_MUST_COPY);
MHD_add_response_header(resp, "Content-Type", "application/json");
add_cors_headers(resp);
return MHD_queue_response(conn, rc == 0 ? MHD_HTTP_OK : MHD_HTTP_INTERNAL_SERVER_ERROR, resp);
} else if (strcmp(url, ROUTE_REPO_LIST) == 0) {
char *resp_buf;
struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf);
if (oom_resp)
return MHD_queue_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, oom_resp);
PldmgrConfig cfg;
config_read(&cfg);
size_t len;
if (cfg.multi_sources_enabled)
len = sources_multi_repository_list_json(resp_buf, RESPONSE_BUFFER_SIZE, 0);
else
len = repository_list_json(resp_buf, RESPONSE_BUFFER_SIZE, 0);
resp = MHD_create_response_from_buffer(len, (void *)resp_buf, MHD_RESPMEM_MUST_FREE);
MHD_add_response_header(resp, "Content-Type", "application/json");
} else if (strcmp(url, ROUTE_REPO_REFRESH) == 0) {
char *resp_buf;
struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf);
if (oom_resp)
return MHD_queue_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, oom_resp);
PldmgrConfig cfg;
config_read(&cfg);
size_t len2;
if (cfg.multi_sources_enabled)
len2 = sources_multi_repository_list_json(resp_buf, RESPONSE_BUFFER_SIZE, 1);
else
len2 = repository_list_json(resp_buf, RESPONSE_BUFFER_SIZE, 1);
resp = MHD_create_response_from_buffer(len2, (void *)resp_buf, MHD_RESPMEM_MUST_FREE);
MHD_add_response_header(resp, "Content-Type", "application/json");
} else if (strcmp(url, ROUTE_REPO_INSTALL) == 0) {
const char *filename = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "filename");
const char *repo_url = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "repo_url");
const char *source_id = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "source_id");
if (!filename) {
const char *err = "{\"ok\":false,\"message\":\"Missing filename\"}";
resp = MHD_create_response_from_buffer(strlen(err), (void *)err, MHD_RESPMEM_MUST_COPY);
MHD_add_response_header(resp, "Content-Type", "application/json");
add_cors_headers(resp);
return MHD_queue_response(conn, MHD_HTTP_BAD_REQUEST, resp);
}
if (!is_safe_filename(filename)) {
const char *err = "{\"ok\":false,\"message\":\"Invalid filename\"}";
resp = MHD_create_response_from_buffer(strlen(err), (void *)err, MHD_RESPMEM_MUST_COPY);
MHD_add_response_header(resp, "Content-Type", "application/json");
add_cors_headers(resp);
return MHD_queue_response(conn, MHD_HTTP_BAD_REQUEST, resp);
}
char msg_buf[1024] = "";
int rc;
if (source_id && source_id[0]) {
rc = sources_multi_repository_install(filename, source_id,
repo_url ? repo_url : "",
msg_buf, sizeof(msg_buf));
} else {
const char *detail = (repo_url && repo_url[0]) ? repo_url : REPOSITORY_SOURCE_URL;
rc = repository_install_download(filename, detail, msg_buf, sizeof(msg_buf));
}
if (msg_buf[0] == '\0') {
snprintf(msg_buf, sizeof(msg_buf), rc == 0 ? "Installed" : "Install failed");
}
char json_resp[1024];
snprintf(json_resp, sizeof(json_resp), "{\"ok\":%s,\"message\":\"%s\"}",
rc == 0 ? "true" : "false", msg_buf);
resp = MHD_create_response_from_buffer(strlen(json_resp), (void *)json_resp, MHD_RESPMEM_MUST_COPY);
MHD_add_response_header(resp, "Content-Type", "application/json");
add_cors_headers(resp);
return MHD_queue_response(conn, rc == 0 ? MHD_HTTP_OK : MHD_HTTP_INTERNAL_SERVER_ERROR, resp);
} else if (strcmp(url, ROUTE_SOURCES_LIST) == 0) {
char *resp_buf;
struct MHD_Response *oom_resp = alloc_response_buffer(&resp_buf);
if (oom_resp)
return MHD_queue_response(conn, MHD_HTTP_INTERNAL_SERVER_ERROR, oom_resp);
sources_list_json(resp_buf, RESPONSE_BUFFER_SIZE);
resp = MHD_create_response_from_buffer(strlen(resp_buf), (void *)resp_buf, MHD_RESPMEM_MUST_FREE);
MHD_add_response_header(resp, "Content-Type", "application/json");
} else if (strcmp(url, ROUTE_SOURCES_ADD) == 0) {
const char *src_url = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "url");
if (!src_url || !src_url[0]) {
const char *err = "{\"ok\":false,\"message\":\"Missing url parameter\"}";
resp = MHD_create_response_from_buffer(strlen(err), (void *)err, MHD_RESPMEM_MUST_COPY);
MHD_add_response_header(resp, "Content-Type", "application/json");
add_cors_headers(resp);
return MHD_queue_response(conn, MHD_HTTP_BAD_REQUEST, resp);
}
char msg_buf_src[512] = "";
int rc_src = sources_add(src_url, msg_buf_src, sizeof(msg_buf_src));
char json_resp_src[1024];
if (rc_src == 0) {
char name_escaped[512];
pldmgr_json_escape(msg_buf_src, name_escaped, sizeof(name_escaped));
snprintf(json_resp_src, sizeof(json_resp_src),
"{\"ok\":true,\"name\":\"%s\"}", name_escaped);
} else {
char msg_escaped[512];
pldmgr_json_escape(msg_buf_src, msg_escaped, sizeof(msg_escaped));
snprintf(json_resp_src, sizeof(json_resp_src),
"{\"ok\":false,\"message\":\"%s\"}", msg_escaped);
}
resp = MHD_create_response_from_buffer(strlen(json_resp_src), (void *)json_resp_src, MHD_RESPMEM_MUST_COPY);
MHD_add_response_header(resp, "Content-Type", "application/json");
add_cors_headers(resp);
return MHD_queue_response(conn, rc_src == 0 ? MHD_HTTP_OK : MHD_HTTP_BAD_REQUEST, resp);
} else if (strcmp(url, ROUTE_SOURCES_REMOVE) == 0) {
const char *idx_str = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "index");
if (!idx_str) {
const char *err = "{\"ok\":false,\"message\":\"Missing index parameter\"}";
resp = MHD_create_response_from_buffer(strlen(err), (void *)err, MHD_RESPMEM_MUST_COPY);
MHD_add_response_header(resp, "Content-Type", "application/json");
add_cors_headers(resp);
return MHD_queue_response(conn, MHD_HTTP_BAD_REQUEST, resp);
}
int idx = atoi(idx_str);
char msg_rm[256] = "";
int rc_rm = sources_remove(idx, msg_rm, sizeof(msg_rm));
char json_rm[512];
char msg_rm_e[256];
pldmgr_json_escape(msg_rm, msg_rm_e, sizeof(msg_rm_e));
snprintf(json_rm, sizeof(json_rm), "{\"ok\":%s,\"message\":\"%s\"}",
rc_rm == 0 ? "true" : "false", msg_rm_e);
resp = MHD_create_response_from_buffer(strlen(json_rm), (void *)json_rm, MHD_RESPMEM_MUST_COPY);
MHD_add_response_header(resp, "Content-Type", "application/json");
add_cors_headers(resp);
return MHD_queue_response(conn, rc_rm == 0 ? MHD_HTTP_OK : MHD_HTTP_BAD_REQUEST, resp);
} else if (strncmp(url, ROUTE_LOAD_PAYLOAD, strlen(ROUTE_LOAD_PAYLOAD)) == 0) {
const char *path = url + strlen(ROUTE_LOAD_PAYLOAD);
const char *final_path = NULL;
char resolved_path[512];
if (is_allowed_payload_path(path)) {
final_path = path;
} else {
const char *filename = strrchr(path, '/');
filename = filename ? filename + 1 : path;
if (is_safe_filename(filename) &&
payload_mgr_resolve_path(filename, resolved_path,
sizeof(resolved_path)) == 0) {
final_path = resolved_path;
}
}
if (!final_path) {
pldmgr_log("[PLDMGR] !!! Payload path rejected: %s\n", path);
const char *err = "Invalid payload name\n";
resp = MHD_create_response_from_buffer(strlen(err), (void *)err,
MHD_RESPMEM_PERSISTENT);
http_status = MHD_HTTP_BAD_REQUEST;
} else if (ps5_launch_elf(final_path) == 0) {
resp = MHD_create_response_from_buffer(strlen(MSG_OK), (void *)MSG_OK,
MHD_RESPMEM_PERSISTENT);
} else {
const char *err = "Failed to launch payload\n";
resp = MHD_create_response_from_buffer(strlen(err), (void *)err,
MHD_RESPMEM_PERSISTENT);
http_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
}
MHD_add_response_header(resp, "Content-Type", "text/plain");
} else if (strncmp(url, ROUTE_DELETE, strlen(ROUTE_DELETE)) == 0) {
const char *filename =
MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "filename");
if (filename) {
if (strstr(filename, "/") || strstr(filename, "..")) {
const char *err = "Invalid filename\n";
resp = MHD_create_response_from_buffer(strlen(err), (void *)err,
MHD_RESPMEM_PERSISTENT);
} else {
if (payload_mgr_delete_payload_file(filename) == 0) {
pldmgr_log("[PLDMGR] Deleted payload: %s\n", filename);
resp = MHD_create_response_from_buffer(strlen(MSG_OK), (void *)MSG_OK,
MHD_RESPMEM_PERSISTENT);
} else {
const char *err = "Failed to delete file\n";
resp = MHD_create_response_from_buffer(strlen(err), (void *)err,
MHD_RESPMEM_PERSISTENT);
}
}
} else {
const char *err = "Missing filename\n";
resp = MHD_create_response_from_buffer(strlen(err), (void *)err,
MHD_RESPMEM_PERSISTENT);
}
MHD_add_response_header(resp, "Content-Type", "text/plain");
} else if (strcmp(url, ROUTE_SHUTDOWN) == 0) {
const char *msg = "Payload Manager Core shutting down...\n";
pldmgr_log("[PLDMGR] %s", msg);
resp = MHD_create_response_from_buffer(strlen(msg), (void *)msg,
MHD_RESPMEM_PERSISTENT);
MHD_add_response_header(resp, "Content-Type", "text/plain");
http_keep_running = 0;
} else if (strcmp(url, ROUTE_LOG) == 0) {
char *resp_buf;
resp = alloc_response_buffer(&resp_buf);
if (!resp) {
size_t pos = log_build_json(resp_buf, RESPONSE_BUFFER_SIZE);
resp = MHD_create_response_from_buffer(pos, (void *)resp_buf,
MHD_RESPMEM_MUST_FREE);
MHD_add_response_header(resp, "Content-Type", "application/json");
}
} else if (strcmp(url, ROUTE_VERSION) == 0) {
resp = MHD_create_response_from_buffer(
strlen(MENU_VERSION), (void *)MENU_VERSION, MHD_RESPMEM_PERSISTENT);
MHD_add_response_header(resp, "Content-Type", "text/plain");
} else if (strcmp(url, ROUTE_GETIP) == 0) {
char ip[64];
if (pldmgr_get_local_ip(ip, sizeof(ip)) != 0) {
strcpy(ip, "0.0.0.0");
}
resp = MHD_create_response_from_buffer(strlen(ip), (void *)ip,
MHD_RESPMEM_MUST_COPY);
MHD_add_response_header(resp, "Content-Type", "text/plain");
} else if (strcmp(url, ROUTE_AUTOLOAD_STATUS) == 0) {
int total, done;
char current[128];
pldmgr_autoload_get_status(&total, &done, current);
int remaining = pldmgr_autoload_get_remaining_seconds();
long long remaining_ms = pldmgr_autoload_get_remaining_ms();
char list_buf[4096] = "";
FILE *f = fopen(AUTOLOAD_CONFIG_PATH, "r");
if (f) {
char line[256];
int first = 1;
while (fgets(line, sizeof(line), f)) {
line[strcspn(line, "\r\n")] = 0;
if (strlen(line) == 0 || line[0] == '!')
continue;
if (!first) strcat(list_buf, ",");
strcat(list_buf, line);
first = 0;
}
fclose(f);
}
PldmgrConfig cfg;
config_read(&cfg);
char current_escaped[256];
char list_escaped[8192];
pldmgr_json_escape(current, current_escaped, sizeof(current_escaped));
pldmgr_json_escape(list_buf, list_escaped, sizeof(list_escaped));
char *resp_buf;
resp = alloc_response_buffer(&resp_buf);
if (!resp) {
snprintf(resp_buf, RESPONSE_BUFFER_SIZE,
"{\"remaining\":%d,\"remaining_ms\":%lld,\"total\":%d,\"done\":%d,\"current\":\"%s\","
"\"list\":\"%s\",\"delay\":%d,\"KILL_DISC_PLAYER_ON_STARTUP\":%s,\"SCAN_USB_PAYLOADS\":%s}",
remaining, remaining_ms, total, done, current_escaped, list_escaped, cfg.autoload_delay,
cfg.kill_disc_player ? "true" : "false", cfg.scan_usb_payloads ? "true" : "false");
resp = MHD_create_response_from_buffer(strlen(resp_buf),
(void *)resp_buf,
MHD_RESPMEM_MUST_FREE);
MHD_add_response_header(resp, "Content-Type", "application/json");
}
} else if (strcmp(url, ROUTE_AUTOLOAD_CLEAR) == 0) {
pldmgr_autoload_reset();
const char *msg = "Autoload status cleared.\n";
resp = MHD_create_response_from_buffer(strlen(msg), (void *)msg,
MHD_RESPMEM_PERSISTENT);
MHD_add_response_header(resp, "Content-Type", "text/plain");
} else if (strcmp(url, ROUTE_ABORT) == 0) {
pldmgr_autoload_abort();
const char *msg = "Autoload sequence aborted.\n";
resp = MHD_create_response_from_buffer(strlen(msg), (void *)msg,
MHD_RESPMEM_PERSISTENT);
MHD_add_response_header(resp, "Content-Type", "text/plain");
} else if (strcmp(url, "/autoload_status") == 0) {
char resp_buf[64];
snprintf(resp_buf, sizeof(resp_buf), "{\"remaining\":%d}",
pldmgr_autoload_get_remaining_seconds());
resp = MHD_create_response_from_buffer(strlen(resp_buf),
(void *)resp_buf,
MHD_RESPMEM_MUST_COPY);
MHD_add_response_header(resp, "Content-Type", "application/json");
} else if (strcmp(url, ROUTE_GET_CONFIG) == 0) {
PldmgrConfig cfg;
config_read(&cfg);
/* Get list */
char list_buf[4096] = {0};
FILE *f = fopen(AUTOLOAD_CONFIG_PATH, "r");
if (f) {
char line[256];
int first = 1;
while (fgets(line, sizeof(line), f)) {
line[strcspn(line, "\r\n")] = 0;
if (strlen(line) == 0)
continue;
if (!first)
strncat(list_buf, ",", sizeof(list_buf) - strlen(list_buf) - 1);
strncat(list_buf, line, sizeof(list_buf) - strlen(list_buf) - 1);
first = 0;
}
fclose(f);
}
char list_escaped[8192];
pldmgr_json_escape(list_buf, list_escaped, sizeof(list_escaped));
char *resp_buf;
resp = alloc_response_buffer(&resp_buf);
if (!resp) {
snprintf(resp_buf, RESPONSE_BUFFER_SIZE,
"{\"AUTOLOAD_ENABLED\":%s,\"AUTOLOAD_LIST\":\"%s\",\"LAST_"
"REPOSITORY_UPDATE\":%ld,"
"\"AUTO_BROWSER_OPEN\":%s,\"AUTOLOAD_DELAY\":%d,\"KILL_DISC_"
"PLAYER_ON_STARTUP\":%s,\"SCAN_USB_PAYLOADS\":%s,\"AUTO_INSTALL_APP\":%s,"
"\"MULTI_SOURCES_ENABLED\":%s}",
cfg.autoload_enabled ? "true" : "false", list_escaped, cfg.last_repository_update,
cfg.auto_browser_open ? "true" : "false", cfg.autoload_delay,
cfg.kill_disc_player ? "true" : "false", cfg.scan_usb_payloads ? "true" : "false",
cfg.auto_install_app ? "true" : "false",
cfg.multi_sources_enabled ? "true" : "false");
resp = MHD_create_response_from_buffer(strlen(resp_buf),
(void *)resp_buf,
MHD_RESPMEM_MUST_FREE);
MHD_add_response_header(resp, "Content-Type", "application/json");
}
} else if (strcmp(url, "/events") == 0) {
struct LogSSEConnection *sse = malloc(sizeof(struct LogSSEConnection));
sse->last_log_version = 0;
sse->sent_initial = 0;
resp = MHD_create_response_from_callback(
MHD_SIZE_UNKNOWN, 1024, &log_stream_callback, sse, &log_stream_cleanup);
MHD_add_response_header(resp, "Content-Type", "text/event-stream");
MHD_add_response_header(resp, "Cache-Control", "no-cache");
MHD_add_response_header(resp, "Connection", "keep-alive");
} else {
/* Default: 404 */
const char *not_found = "404 Not Found\n";
resp = MHD_create_response_from_buffer(strlen(not_found), (void *)not_found,
MHD_RESPMEM_PERSISTENT);
MHD_add_response_header(resp, "Content-Type", "text/plain");
}
if (!resp)
return MHD_NO;
add_cors_headers(resp);
ret = MHD_queue_response(conn, http_status, resp);
MHD_destroy_response(resp);
return ret;
}
+165
View File
@@ -0,0 +1,165 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <ctype.h>
#include <sys/stat.h>
#include "json_helpers.h"
int json_append(JsonListBuilder *jb, const char *fmt, ...) {
va_list args;
int written;
if (jb->pos >= jb->size) {
return -1;
}
va_start(args, fmt);
written = vsnprintf(jb->buf + jb->pos, jb->size - jb->pos, fmt, args);
va_end(args);
if (written < 0) {
return -1;
}
if ((size_t)written >= jb->size - jb->pos) {
jb->pos = jb->size - 1;
jb->buf[jb->pos] = '\0';
return -1;
}
jb->pos += (size_t)written;
return 0;
}
int json_extract_string(const char *obj_start, const char *obj_end,
const char *key, char *out, size_t out_size) {
char key_pattern[96];
const char *p;
const char *colon;
const char *q;
size_t pos = 0;
if (out_size == 0) {
return -1;
}
out[0] = '\0';
snprintf(key_pattern, sizeof(key_pattern), "\"%s\"", key);
p = strstr(obj_start, key_pattern);
if (!p || p >= obj_end) {
return -1;
}
colon = strchr(p + strlen(key_pattern), ':');
if (!colon || colon >= obj_end) {
return -1;
}
q = colon + 1;
while (q < obj_end && isspace((unsigned char)*q)) {
q++;
}
if (q >= obj_end || *q != '"') {
return -1;
}
q++;
while (q < obj_end && *q != '"') {
if (*q == '\\' && (q + 1) < obj_end) {
q++;
}
if (pos + 1 < out_size) {
out[pos++] = *q;
}
q++;
}
out[pos] = '\0';
return 0;
}
int read_file_text(const char *path, char **out_buf, size_t *out_size) {
FILE *f;
long fsize;
char *buf;
size_t nread;
*out_buf = NULL;
*out_size = 0;
f = fopen(path, "rb");
if (!f) {
return -1;
}
if (fseek(f, 0, SEEK_END) != 0) {
fclose(f);
return -1;
}
fsize = ftell(f);
if (fsize < 0) {
fclose(f);
return -1;
}
if (fseek(f, 0, SEEK_SET) != 0) {
fclose(f);
return -1;
}
buf = (char *)malloc((size_t)fsize + 1);
if (!buf) {
fclose(f);
return -1;
}
nread = fread(buf, 1, (size_t)fsize, f);
fclose(f);
buf[nread] = '\0';
*out_buf = buf;
*out_size = nread;
return 0;
}
int write_file_text(const char *path, const char *data, size_t size) {
FILE *f = fopen(path, "wb");
if (!f) {
return -1;
}
if (fwrite(data, 1, size, f) != size) {
fclose(f);
return -1;
}
fclose(f);
return 0;
}
int mkdir_if_missing(const char *path) {
struct stat st;
if (stat(path, &st) == 0) {
return S_ISDIR(st.st_mode) ? 0 : -1;
}
return mkdir(path, 0777);
}
int ensure_dir_recursive(const char *path) {
char tmp[512];
size_t len = strlen(path);
if (len >= sizeof(tmp)) {
return -1;
}
strncpy(tmp, path, sizeof(tmp));
tmp[len] = '\0';
for (size_t i = 1; i < len; i++) {
if (tmp[i] == '/') {
tmp[i] = '\0';
if (strlen(tmp) > 0 && mkdir_if_missing(tmp) != 0) {
return -1;
}
tmp[i] = '/';
}
}
return mkdir_if_missing(tmp);
}
+128
View File
@@ -0,0 +1,128 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <pthread.h>
#include <errno.h>
#include <time.h>
#include "log_server.h"
#include "utils.h"
/* Ring buffer state */
static char log_buffer[MAX_LOG_LINES][MAX_LOG_LINE_LEN];
static int log_head = 0;
static int log_count = 0;
static pthread_mutex_t log_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t log_cond = PTHREAD_COND_INITIALIZER;
static int log_updated = 0;
static volatile int server_active_flag = 0;
void log_server_set_active(void) { server_active_flag = 1; }
int pldmgr_server_is_active(void) { return server_active_flag; }
void pldmgr_log(const char *fmt, ...) {
char line[MAX_LOG_LINE_LEN];
va_list args;
va_start(args, fmt);
vsnprintf(line, sizeof(line), fmt, args);
va_end(args);
/* Print to stdout */
printf("%s", line);
/* Remove trailing newline for internal storage if present */
size_t len = strlen(line);
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) {
line[len - 1] = '\0';
len--;
}
if (len == 0)
return;
/* Add to circular buffer with lock */
pthread_mutex_lock(&log_mutex);
strncpy(log_buffer[log_head], line, MAX_LOG_LINE_LEN);
log_head = (log_head + 1) % MAX_LOG_LINES;
if (log_count < MAX_LOG_LINES)
log_count++;
/* Signal SSE clients */
log_updated++;
pthread_cond_broadcast(&log_cond);
pthread_mutex_unlock(&log_mutex);
}
ssize_t log_stream_callback(void *cls, uint64_t pos, char *buf, size_t max) {
struct LogSSEConnection *conn = (struct LogSSEConnection *)cls;
pthread_mutex_lock(&log_mutex);
/* Initial catch-up */
if (!conn->sent_initial) {
char initial_batch[MAX_LOG_LINES * (MAX_LOG_LINE_LEN + 16)];
size_t offset = 0;
for (int i = 0; i < log_count; i++) {
int idx = (log_head - log_count + i + MAX_LOG_LINES) % MAX_LOG_LINES;
offset += snprintf(initial_batch + offset, sizeof(initial_batch) - offset,
"data: %s\n\n", log_buffer[idx]);
}
conn->sent_initial = 1;
conn->last_log_version = log_updated;
if (offset > 0) {
size_t to_copy = (offset < max) ? offset : max;
memcpy(buf, initial_batch, to_copy);
pthread_mutex_unlock(&log_mutex);
return to_copy;
}
}
/* Wait for new logs */
while (conn->last_log_version == log_updated) {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += 5; /* Keep-alive ping interval */
if (pthread_cond_timedwait(&log_cond, &log_mutex, &ts) == ETIMEDOUT) {
/* Send a comment keep-alive */
const char *ping = ": ping\n\n";
memcpy(buf, ping, strlen(ping));
pthread_mutex_unlock(&log_mutex);
return strlen(ping);
}
}
/* Send new log */
int idx = (log_head - 1 + MAX_LOG_LINES) % MAX_LOG_LINES;
size_t len = snprintf(buf, max, "data: %s\n\n", log_buffer[idx]);
conn->last_log_version = log_updated;
pthread_mutex_unlock(&log_mutex);
return len;
}
void log_stream_cleanup(void *cls) { free(cls); }
size_t log_build_json(char *buf, size_t buf_size) {
size_t bpos = 0;
bpos += snprintf(buf + bpos, buf_size - bpos, "{\"logs\":[");
pthread_mutex_lock(&log_mutex);
for (int i = 0; i < log_count; i++) {
int idx = (log_head - log_count + i + MAX_LOG_LINES) % MAX_LOG_LINES;
char escaped[MAX_LOG_LINE_LEN * 2];
pldmgr_json_escape(log_buffer[idx], escaped, sizeof(escaped));
bpos += snprintf(buf + bpos, buf_size - bpos,
"\"%s\"%s", escaped,
(i == log_count - 1) ? "" : ",");
}
pthread_mutex_unlock(&log_mutex);
bpos += snprintf(buf + bpos, buf_size - bpos, "]}");
return bpos;
}
+186 -1490
View File
File diff suppressed because it is too large. Load diff
+405 -2292
View File
File diff suppressed because it is too large. Load diff
+101
View File
@@ -0,0 +1,101 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/proc.h>
#include <sys/user.h>
#include <sys/sysctl.h>
#include <stdint.h>
#include "process_mgr.h"
#include "json_helpers.h"
#include "pldmgr.h"
#define MiB(x) ((x) / (1024.0 * 1024))
typedef struct app_info {
uint32_t app_id;
uint64_t unknown1;
char title_id[14];
char unknown2[0x3c];
} app_info_t;
extern int sceKernelGetAppInfo(pid_t pid, app_info_t *info);
static int is_user_daemon(const char *name, uint32_t app_id) {
if (!name) return 0;
/* Specifically exclude mini-syscore.elf */
if (strcmp(name, "mini-syscore.elf") == 0) return 0;
/* Must have app_id == 0000 */
if (app_id != 0) return 0;
const char *ext = strrchr(name, '.');
if (ext && strcasecmp(ext, ".elf") == 0) return 1;
return 0;
}
size_t process_list_json(char *buf, size_t max_size) {
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PROC, 0};
size_t buf_size = 0;
void *sysctl_buf = NULL;
JsonListBuilder jb = { buf, max_size, 0, 1 };
buf[0] = '\0';
json_append(&jb, "{\"processes\":[\n");
if (sysctl(mib, 4, NULL, &buf_size, NULL, 0) == 0) {
sysctl_buf = malloc(buf_size);
if (sysctl_buf) {
if (sysctl(mib, 4, sysctl_buf, &buf_size, NULL, 0) == 0) {
int count = 0;
for (void *ptr = sysctl_buf; ptr < (sysctl_buf + buf_size);) {
struct kinfo_proc *ki = (struct kinfo_proc*)ptr;
if (ki->ki_structsize == 0) break;
ptr += ki->ki_structsize;
app_info_t appinfo;
if(sceKernelGetAppInfo(ki->ki_pid, &appinfo)) {
memset(&appinfo, 0, sizeof(appinfo));
}
int is_daemon = is_user_daemon(ki->ki_comm, appinfo.app_id);
char name_e[512];
pldmgr_json_escape(ki->ki_comm, name_e, sizeof(name_e));
double mem_mib = MiB(ki->ki_rssize * PAGE_SIZE);
if (json_append(&jb, "%s {\"pid\":%d,\"name\":\"%s\",\"memory\":%.1f,\"is_daemon\":%s}",
(count > 0) ? ",\n" : "",
(int)ki->ki_pid, name_e, mem_mib, is_daemon ? "true" : "false") != 0) {
break;
}
count++;
}
}
free(sysctl_buf);
}
}
json_append(&jb, "\n]}\n");
return jb.pos;
}
int process_kill(int pid) {
if (pid <= 0) return -1; /* Prevent killing kernel or init */
/* Prevent killing our own process */
if (pid == getpid()) return -1;
if (kill(pid, SIGKILL) == 0) {
return 0;
}
return -1;
}
+676
View File
@@ -0,0 +1,676 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <ctype.h>
#include <time.h>
#include <dirent.h>
#include <unistd.h>
#include <sys/stat.h>
#include <curl/curl.h>
#include "repository.h"
#include "sha256.h"
#include "json_helpers.h"
#include "config.h"
#include "pldmgr.h"
#include "payload_mgr.h"
#include "assets_cacert_pem.h"
/* ── cURL download helper ──────────────────────────────────── */
static size_t write_data(void *ptr, size_t size, size_t nmemb, FILE *stream) {
return fwrite(ptr, size, nmemb, stream);
}
int download_to_file(const char *url, const char *out_path) {
CURL *curl;
FILE *fp;
CURLcode res = CURLE_FAILED_INIT;
if (!url || !out_path) {
pldmgr_log("[PLDMGR] download_to_file: missing url or path\n");
return -1;
}
fp = fopen(out_path, "wb");
if (!fp) {
pldmgr_log("[PLDMGR] download_to_file: failed to open %s\n", out_path);
return -1;
}
curl = curl_easy_init();
if (curl) {
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_data);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
/* User agent */
curl_easy_setopt(curl, CURLOPT_USERAGENT, "pldmgr/1.0");
/* Securely verify SSL against embedded CA bundle */
struct curl_blob blob;
blob.data = (void *)assets_cacert_pem;
blob.len = assets_cacert_pem_len;
blob.flags = CURL_BLOB_COPY;
curl_easy_setopt(curl, CURLOPT_CAINFO_BLOB, &blob);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
/* Allow redirection */
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
res = curl_easy_perform(curl);
long http_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
curl_easy_cleanup(curl);
if (res != CURLE_OK) {
pldmgr_log("[PLDMGR] curl_easy_perform failed: %s url=%s\n",
curl_easy_strerror(res), url);
} else if (http_code != 200) {
pldmgr_log("[PLDMGR] HTTP status %ld for %s\n", http_code, url);
res = CURLE_HTTP_RETURNED_ERROR;
}
}
fclose(fp);
if (res != CURLE_OK) {
remove(out_path);
return -1;
}
return 0;
}
/* ── JSON parsing ──────────────────────────────────────────── */
int parse_repository_payloads(const char *json, RepoPayload **out_items, size_t *out_count) {
const char *p = json;
RepoPayload *items = NULL;
size_t count = 0;
*out_items = NULL;
*out_count = 0;
while ((p = strchr(p, '{')) != NULL) {
const char *end = strchr(p, '}');
RepoPayload item;
RepoPayload *tmp;
if (!end)
break;
memset(&item, 0, sizeof(item));
if (json_extract_string(p, end, "name", item.name, sizeof(item.name)) != 0 ||
json_extract_string(p, end, "filename", item.filename, sizeof(item.filename)) != 0 ||
json_extract_string(p, end, "url", item.url, sizeof(item.url)) != 0) {
p = end + 1;
continue;
}
json_extract_string(p, end, "source", item.source, sizeof(item.source));
json_extract_string(p, end, "source_direct", item.source_direct, sizeof(item.source_direct));
json_extract_string(p, end, "description", item.description, sizeof(item.description));
json_extract_string(p, end, "last_update", item.last_update, sizeof(item.last_update));
json_extract_string(p, end, "version", item.version, sizeof(item.version));
json_extract_string(p, end, "checksum", item.checksum, sizeof(item.checksum));
tmp = (RepoPayload *)realloc(items, (count + 1) * sizeof(RepoPayload));
if (!tmp) {
free(items);
return -1;
}
items = tmp;
items[count] = item;
count++;
p = end + 1;
}
*out_items = items;
*out_count = count;
return 0;
}
static int load_cached_repository(RepoPayload **out_items, size_t *out_count) {
char *json = NULL;
size_t size = 0;
int ret;
if (read_file_text(REPOSITORY_CACHE_PATH, &json, &size) != 0 || !json || size == 0) {
if (json)
free(json);
return -1;
}
ret = parse_repository_payloads(json, out_items, out_count);
free(json);
return ret;
}
/* ── Payload metadata sidecar ──────────────────────────────── */
int write_payload_details_json(const RepoPayload *item, const char *details_path,
const char *install_source, const char *install_source_detail) {
char name[256], filename[384], url[1400], source[1400], source_direct[1400];
char description[1400], last_update[128], version[128], checksum[128];
char downloaded_at[64], i_src[256], i_detail[1400], i_src_name[256];
char json_buf[8192];
time_t now = time(NULL);
struct tm tmv;
memset(&tmv, 0, sizeof(tmv));
localtime_r(&now, &tmv);
strftime(downloaded_at, sizeof(downloaded_at), "%Y-%m-%dT%H:%M:%S%z", &tmv);
pldmgr_json_escape(item->name, name, sizeof(name));
pldmgr_json_escape(item->filename, filename, sizeof(filename));
pldmgr_json_escape(item->url, url, sizeof(url));
pldmgr_json_escape(item->source, source, sizeof(source));
pldmgr_json_escape(item->source_direct, source_direct, sizeof(source_direct));
pldmgr_json_escape(item->description, description, sizeof(description));
pldmgr_json_escape(item->last_update, last_update, sizeof(last_update));
pldmgr_json_escape(item->version, version, sizeof(version));
pldmgr_json_escape(item->checksum, checksum, sizeof(checksum));
pldmgr_json_escape(install_source ? install_source : "unknown", i_src, sizeof(i_src));
pldmgr_json_escape(install_source_detail ? install_source_detail : "", i_detail, sizeof(i_detail));
pldmgr_json_escape(item->source_name[0] ? item->source_name : "", i_src_name, sizeof(i_src_name));
snprintf(json_buf, sizeof(json_buf),
"{\n"
" \"name\": \"%s\",\n"
" \"filename\": \"%s\",\n"
" \"url\": \"%s\",\n"
" \"source\": \"%s\",\n"
" \"source_direct\": \"%s\",\n"
" \"description\": \"%s\",\n"
" \"last_update\": \"%s\",\n"
" \"version\": \"%s\",\n"
" \"checksum\": \"%s\",\n"
" \"downloaded_at\": \"%s\",\n"
" \"install_source\": \"%s\",\n"
" \"install_source_detail\": \"%s\",\n"
" \"source_name\": \"%s\"\n"
"}\n",
name, filename, url, source, source_direct, description,
last_update, version, checksum, downloaded_at, i_src, i_detail,
i_src_name);
FILE *f = fopen(details_path, "w");
if (!f) return -1;
fwrite(json_buf, 1, strlen(json_buf), f);
fclose(f);
return 0;
}
int write_simple_payload_details_json(const char *filename, const char *details_path,
const char *install_source, const char *install_source_detail) {
RepoPayload item;
memset(&item, 0, sizeof(item));
strncpy(item.name, filename, sizeof(item.name) - 1);
strncpy(item.filename, filename, sizeof(item.filename) - 1);
return write_payload_details_json(&item, details_path, install_source, install_source_detail);
}
/* ── Repository cache management ───────────────────────────── */
int repository_push_json(const char *json, size_t len) {
RepoPayload *items = NULL;
size_t count = 0;
char tmp_path[512];
time_t now = time(NULL);
if (!json || len == 0) {
pldmgr_log("[PLDMGR] !!! repository_push: empty body\n");
return -1;
}
if (parse_repository_payloads(json, &items, &count) != 0 || count == 0) {
pldmgr_log("[PLDMGR] !!! repository_push: JSON parse failed or 0 entries\n");
if (items) free(items);
return -1;
}
free(items);
ensure_dir_recursive(BASE_DATA_DIR);
ensure_dir_recursive(PAYLOADS_STORAGE_DIR);
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", REPOSITORY_CACHE_PATH);
if (write_file_text(tmp_path, json, len) != 0) {
remove(tmp_path);
return -1;
}
if (rename(tmp_path, REPOSITORY_CACHE_PATH) != 0) {
remove(tmp_path);
return -1;
}
config_write_last_update((long)now);
pldmgr_log("[PLDMGR] Repository cache updated via browser push (%zu entries)\n", count);
return 0;
}
int repository_ensure_fresh(int force_refresh) {
long last_update = 0;
time_t now = time(NULL);
char tmp_path[512];
char *json = NULL;
size_t json_size = 0;
RepoPayload *items = NULL;
size_t count = 0;
ensure_dir_recursive(BASE_DATA_DIR);
ensure_dir_recursive(PAYLOADS_STORAGE_DIR);
config_read_last_update(&last_update);
if (!force_refresh && access(REPOSITORY_CACHE_PATH, F_OK) == 0) {
long delta = (long)now - last_update;
if (delta >= 0 && delta < REPOSITORY_REFRESH_INTERVAL_SEC) {
return 0;
}
}
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", REPOSITORY_CACHE_PATH);
if (download_to_file(REPOSITORY_SOURCE_URL, tmp_path) != 0) {
return -1;
}
if (read_file_text(tmp_path, &json, &json_size) != 0 || json_size == 0) {
remove(tmp_path);
if (json)
free(json);
return -1;
}
if (parse_repository_payloads(json, &items, &count) != 0 || count == 0) {
pldmgr_log("[PLDMGR] !!! Repository payload JSON parse failed\n");
free(items);
free(json);
remove(tmp_path);
return -1;
}
free(items);
free(json);
if (rename(tmp_path, REPOSITORY_CACHE_PATH) != 0) {
remove(tmp_path);
return -1;
}
config_write_last_update((long)now);
pldmgr_log("[PLDMGR] Repository cache refreshed (%ld entries timestamp)\n", (long)count);
return 0;
}
size_t repository_list_json(char *json_buf, size_t buf_size, int force_refresh) {
char *cached = NULL;
size_t cached_size = 0;
long last_update = 0;
size_t pos = 0;
if (force_refresh) {
if (repository_ensure_fresh(1) != 0) {
config_read_last_update(&last_update);
snprintf(json_buf, buf_size,
"{\"payloads\":[],\"last_update\":%ld,\"cache_status\":\"error\"}",
last_update);
return strlen(json_buf);
}
}
config_read_last_update(&last_update);
if (read_file_text(REPOSITORY_CACHE_PATH, &cached, &cached_size) != 0 || cached_size == 0) {
if (cached)
free(cached);
snprintf(json_buf, buf_size,
"{\"payloads\":[],\"last_update\":%ld,\"cache_status\":\"missing\"}",
last_update);
return strlen(json_buf);
}
pos += (size_t)snprintf(json_buf + pos, buf_size - pos, "{\"payloads\":");
if (pos >= buf_size || buf_size - pos <= 64 || cached_size > (buf_size - pos - 64)) {
snprintf(json_buf, buf_size,
"{\"payloads\":[],\"last_update\":%ld,\"cache_status\":\"truncated\"}",
last_update);
free(cached);
return strlen(json_buf);
}
if (pos < buf_size) {
size_t copy_len = cached_size;
memcpy(json_buf + pos, cached, copy_len);
pos += copy_len;
}
pos += (size_t)snprintf(json_buf + pos, buf_size - pos,
",\"last_update\":%ld,\"cache_status\":\"ok\",\"repo_url\":\"%s\"}",
last_update, REPOSITORY_SOURCE_URL);
free(cached);
return pos;
}
/* ── Install from repository ───────────────────────────────── */
int repository_install_download(const char *filename, const char *install_source_detail,
char *msg_buf, size_t msg_buf_size) {
RepoPayload *items = NULL;
size_t count = 0;
int found = -1;
char tmp_path[512];
if (msg_buf && msg_buf_size > 0)
msg_buf[0] = '\0';
if (!filename || strlen(filename) == 0 || strstr(filename, "/") || strstr(filename, "..")) {
snprintf(msg_buf, msg_buf_size, "Invalid filename");
return -1;
}
if (load_cached_repository(&items, &count) != 0 || count == 0) {
if (repository_ensure_fresh(1) == 0) {
if (load_cached_repository(&items, &count) != 0 || count == 0) {
snprintf(msg_buf, msg_buf_size, "Repository cache missing or invalid");
if (items) free(items);
return -1;
}
} else {
snprintf(msg_buf, msg_buf_size, "Repository cache missing or invalid");
if (items) free(items);
return -1;
}
}
for (size_t i = 0; i < count; i++) {
if (strcmp(items[i].filename, filename) == 0) {
found = (int)i;
break;
}
}
if (found < 0) {
free(items);
snprintf(msg_buf, msg_buf_size, "Payload not found in repository");
return -1;
}
ensure_dir_recursive(PAYLOADS_STORAGE_DIR);
snprintf(tmp_path, sizeof(tmp_path), "%s/%s.part", PAYLOADS_STORAGE_DIR, items[found].filename);
if (download_to_file(items[found].url, tmp_path) != 0) {
free(items);
snprintf(msg_buf, msg_buf_size, "Download failed");
remove(tmp_path);
return -1;
}
const char *detail = (install_source_detail && install_source_detail[0])
? install_source_detail
: REPOSITORY_SOURCE_URL;
if (repository_install_commit(items[found].filename, tmp_path,
"repository", detail,
msg_buf, msg_buf_size) != 0) {
free(items);
return -1;
}
free(items);
return 0;
}
int repository_install_commit(const char *filename, const char *uploaded_temp_path,
const char *install_source, const char *install_source_detail,
char *msg_buf, size_t msg_buf_size) {
RepoPayload *items = NULL;
size_t count = 0;
int found = -1;
char payload_dir[512];
char final_path[640];
char details_path[700];
if (!filename || strlen(filename) == 0 || strstr(filename, "/") || strstr(filename, "..")) {
snprintf(msg_buf, msg_buf_size, "Invalid filename");
return -1;
}
if (load_cached_repository(&items, &count) != 0 || count == 0) {
if (repository_ensure_fresh(1) == 0) {
if (load_cached_repository(&items, &count) != 0 || count == 0) {
snprintf(msg_buf, msg_buf_size, "Repository cache missing or invalid");
if (items) free(items);
return -1;
}
} else {
snprintf(msg_buf, msg_buf_size, "Repository cache missing or invalid");
if (items) free(items);
return -1;
}
}
if (count == 0) {
snprintf(msg_buf, msg_buf_size, "Repository cache missing or invalid");
if (items) free(items);
return -1;
}
for (size_t i = 0; i < count; i++) {
if (strcmp(items[i].filename, filename) == 0) {
found = (int)i;
break;
}
}
if (found < 0) {
free(items);
snprintf(msg_buf, msg_buf_size, "Payload not found in repository");
return -1;
}
char folder_name[128];
pldmgr_utils_get_payload_folder_name(items[found].filename, folder_name, sizeof(folder_name));
snprintf(payload_dir, sizeof(payload_dir), "%s/%s", PAYLOADS_STORAGE_DIR, folder_name);
if (ensure_dir_recursive(payload_dir) != 0) {
free(items);
snprintf(msg_buf, msg_buf_size, "Failed to create payload directory");
return -1;
}
snprintf(final_path, sizeof(final_path), "%s/%s", payload_dir, items[found].filename);
snprintf(details_path, sizeof(details_path), "%s/%s.json", payload_dir, items[found].filename);
if (strlen(items[found].checksum) == 64) {
char calculated[65];
if (compute_sha256_file(uploaded_temp_path, calculated) != 0) {
free(items);
remove(uploaded_temp_path);
snprintf(msg_buf, msg_buf_size, "Checksum computation failed");
return -1;
}
if (strcasecmp(calculated, items[found].checksum) != 0) {
pldmgr_log("[PLDMGR] !!! Checksum mismatch for %s\n", items[found].filename);
free(items);
remove(uploaded_temp_path);
snprintf(msg_buf, msg_buf_size, "Checksum mismatch");
return -1;
}
}
/* Verify succeeded, now clear previous payload and metadata. */
payload_mgr_remove_old_files(payload_dir, items[found].filename);
if (rename(uploaded_temp_path, final_path) != 0) {
free(items);
remove(uploaded_temp_path);
snprintf(msg_buf, msg_buf_size, "Failed to finalize payload file");
return -1;
}
if (write_payload_details_json(&items[found], details_path, install_source, install_source_detail) != 0) {
free(items);
remove(final_path);
snprintf(msg_buf, msg_buf_size, "Failed to write payload metadata");
return -1;
}
pldmgr_log("[PLDMGR] Repository payload installed: %s -> %s\n", items[found].filename, final_path);
snprintf(msg_buf, msg_buf_size, "Installed %s", items[found].filename);
free(items);
return 0;
}
/* ── Self-update ───────────────────────────────────────────── */
int get_elf_pldmgr_version(const char *path, char *out_version, size_t out_size) {
FILE *f = fopen(path, "rb");
if (!f) return -1;
char sig[16];
sig[0] = 'P'; sig[1] = 'L'; sig[2] = 'D'; sig[3] = 'M'; sig[4] = 'G';
sig[5] = 'R'; sig[6] = '_'; sig[7] = 'V'; sig[8] = 'E'; sig[9] = 'R'; sig[10] = ':'; sig[11] = '\0';
size_t sig_len = 11;
char buffer[8192];
size_t bytes_read;
int match_idx = 0;
while ((bytes_read = fread(buffer, 1, sizeof(buffer), f)) > 0) {
for (size_t i = 0; i < bytes_read; i++) {
if (buffer[i] == sig[match_idx]) {
match_idx++;
if (match_idx == (int)sig_len) {
size_t v_idx = 0;
i++;
while (i < bytes_read && buffer[i] != '\0' && v_idx < out_size - 1) {
out_version[v_idx++] = buffer[i++];
}
if (i < bytes_read && buffer[i] == '\0') {
if (v_idx > 0) {
out_version[v_idx] = '\0';
fclose(f);
return 0;
} else {
match_idx = 0;
continue;
}
} else {
while (v_idx < out_size - 1) {
int c = fgetc(f);
if (c == EOF || c == '\0') break;
out_version[v_idx++] = (char)c;
}
out_version[v_idx] = '\0';
if (v_idx > 0) {
fclose(f);
return 0;
} else {
match_idx = 0;
continue;
}
}
}
} else {
match_idx = 0;
if (buffer[i] == sig[0]) match_idx = 1;
}
}
}
fclose(f);
return -1;
}
static int parse_version_part(const char **p) {
int val = 0;
while (**p >= '0' && **p <= '9') {
val = val * 10 + (**p - '0');
(*p)++;
}
if (**p == '.') (*p)++;
return val;
}
static int compare_versions(const char *v1, const char *v2) {
const char *p1 = v1;
const char *p2 = v2;
int maj1 = parse_version_part(&p1);
int maj2 = parse_version_part(&p2);
if (maj1 != maj2) return maj1 - maj2;
int min1 = parse_version_part(&p1);
int min2 = parse_version_part(&p2);
if (min1 != min2) return min1 - min2;
int pat1 = parse_version_part(&p1);
int pat2 = parse_version_part(&p2);
int res = pat1 - pat2;
pldmgr_log("[PLDMGR] Comparing versions: %s vs %s -> %d\n", v1, v2, res);
return res;
}
int repository_check_self_update(char *out_path, size_t out_size) {
DIR *dir = opendir(PAYLOADS_STORAGE_DIR);
if (!dir) return -1;
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
if (entry->d_name[0] == '.') continue;
/* Check for pldmgr or payload-manager directory (case-insensitive) */
if (strcasecmp(entry->d_name, "pldmgr") == 0 || strcasecmp(entry->d_name, "payload-manager") == 0) {
char subdir_path[512];
snprintf(subdir_path, sizeof(subdir_path), "%s/%s", PAYLOADS_STORAGE_DIR, entry->d_name);
struct stat st;
if (stat(subdir_path, &st) == 0 && S_ISDIR(st.st_mode)) {
DIR *sdir = opendir(subdir_path);
if (sdir) {
struct dirent *sentry;
while ((sentry = readdir(sdir)) != NULL) {
if (sentry->d_name[0] == '.') continue;
/* Look for .elf or .bin files that match our name */
if (strstr(sentry->d_name, ".elf") || strstr(sentry->d_name, ".bin")) {
if (strcasestr(sentry->d_name, "pldmgr") || strcasestr(sentry->d_name, "payload-manager")) {
char full_path[512];
snprintf(full_path, sizeof(full_path), "%s/%s", subdir_path, sentry->d_name);
/* Verify ELF Magic */
unsigned char magic[4];
FILE *ef = fopen(full_path, "rb");
if (ef) {
if (fread(magic, 1, 4, ef) == 4 && magic[0] == 0x7f && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F') {
fclose(ef);
char version[64];
if (get_elf_pldmgr_version(full_path, version, sizeof(version)) == 0) {
pldmgr_log("[PLDMGR] Found potential update: %s (v%s)\n", full_path, version);
if (compare_versions(version, MENU_VERSION) > 0) {
strncpy(out_path, full_path, out_size);
closedir(sdir);
closedir(dir);
return 0;
}
}
} else {
fclose(ef);
}
}
}
}
}
closedir(sdir);
}
}
}
}
closedir(dir);
return -1;
}
+164
View File
@@ -0,0 +1,164 @@
#include <stdio.h>
#include <string.h>
#include "sha256.h"
static const unsigned int sha256_k[64] = {
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
};
#define SHA256_ROTR(a, b) (((a) >> (b)) | ((a) << (32 - (b))))
#define SHA256_CH(x, y, z) (((x) & (y)) ^ (~(x) & (z)))
#define SHA256_MAJ(x, y, z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z)))
#define SHA256_EP0(x) (SHA256_ROTR(x, 2) ^ SHA256_ROTR(x, 13) ^ SHA256_ROTR(x, 22))
#define SHA256_EP1(x) (SHA256_ROTR(x, 6) ^ SHA256_ROTR(x, 11) ^ SHA256_ROTR(x, 25))
#define SHA256_SIG0(x) (SHA256_ROTR(x, 7) ^ SHA256_ROTR(x, 18) ^ ((x) >> 3))
#define SHA256_SIG1(x) (SHA256_ROTR(x, 17) ^ SHA256_ROTR(x, 19) ^ ((x) >> 10))
static void sha256_transform(SHA256_CTX *ctx, const unsigned char data[]) {
unsigned int a, b, c, d, e, f, g, h, i, j, t1, t2, m[64];
for (i = 0, j = 0; i < 16; i++, j += 4) {
m[i] = ((unsigned int)data[j] << 24) | ((unsigned int)data[j + 1] << 16) |
((unsigned int)data[j + 2] << 8) | ((unsigned int)data[j + 3]);
}
for (; i < 64; i++) {
m[i] = SHA256_SIG1(m[i - 2]) + m[i - 7] + SHA256_SIG0(m[i - 15]) + m[i - 16];
}
a = ctx->state[0];
b = ctx->state[1];
c = ctx->state[2];
d = ctx->state[3];
e = ctx->state[4];
f = ctx->state[5];
g = ctx->state[6];
h = ctx->state[7];
for (i = 0; i < 64; i++) {
t1 = h + SHA256_EP1(e) + SHA256_CH(e, f, g) + sha256_k[i] + m[i];
t2 = SHA256_EP0(a) + SHA256_MAJ(a, b, c);
h = g;
g = f;
f = e;
e = d + t1;
d = c;
c = b;
b = a;
a = t1 + t2;
}
ctx->state[0] += a;
ctx->state[1] += b;
ctx->state[2] += c;
ctx->state[3] += d;
ctx->state[4] += e;
ctx->state[5] += f;
ctx->state[6] += g;
ctx->state[7] += h;
}
void sha256_init(SHA256_CTX *ctx) {
ctx->datalen = 0;
ctx->bitlen = 0;
ctx->state[0] = 0x6a09e667;
ctx->state[1] = 0xbb67ae85;
ctx->state[2] = 0x3c6ef372;
ctx->state[3] = 0xa54ff53a;
ctx->state[4] = 0x510e527f;
ctx->state[5] = 0x9b05688c;
ctx->state[6] = 0x1f83d9ab;
ctx->state[7] = 0x5be0cd19;
}
void sha256_update(SHA256_CTX *ctx, const unsigned char data[], size_t len) {
size_t i;
for (i = 0; i < len; i++) {
ctx->data[ctx->datalen] = data[i];
ctx->datalen++;
if (ctx->datalen == 64) {
sha256_transform(ctx, ctx->data);
ctx->bitlen += 512;
ctx->datalen = 0;
}
}
}
void sha256_final(SHA256_CTX *ctx, unsigned char hash[]) {
unsigned int i = ctx->datalen;
if (ctx->datalen < 56) {
ctx->data[i++] = 0x80;
while (i < 56) {
ctx->data[i++] = 0x00;
}
} else {
ctx->data[i++] = 0x80;
while (i < 64) {
ctx->data[i++] = 0x00;
}
sha256_transform(ctx, ctx->data);
memset(ctx->data, 0, 56);
}
ctx->bitlen += (unsigned long long)ctx->datalen * 8ULL;
ctx->data[63] = (unsigned char)(ctx->bitlen);
ctx->data[62] = (unsigned char)(ctx->bitlen >> 8);
ctx->data[61] = (unsigned char)(ctx->bitlen >> 16);
ctx->data[60] = (unsigned char)(ctx->bitlen >> 24);
ctx->data[59] = (unsigned char)(ctx->bitlen >> 32);
ctx->data[58] = (unsigned char)(ctx->bitlen >> 40);
ctx->data[57] = (unsigned char)(ctx->bitlen >> 48);
ctx->data[56] = (unsigned char)(ctx->bitlen >> 56);
sha256_transform(ctx, ctx->data);
for (i = 0; i < 4; i++) {
hash[i] = (ctx->state[0] >> (24 - i * 8)) & 0x000000ff;
hash[i + 4] = (ctx->state[1] >> (24 - i * 8)) & 0x000000ff;
hash[i + 8] = (ctx->state[2] >> (24 - i * 8)) & 0x000000ff;
hash[i + 12] = (ctx->state[3] >> (24 - i * 8)) & 0x000000ff;
hash[i + 16] = (ctx->state[4] >> (24 - i * 8)) & 0x000000ff;
hash[i + 20] = (ctx->state[5] >> (24 - i * 8)) & 0x000000ff;
hash[i + 24] = (ctx->state[6] >> (24 - i * 8)) & 0x000000ff;
hash[i + 28] = (ctx->state[7] >> (24 - i * 8)) & 0x000000ff;
}
}
int compute_sha256_file(const char *path, char out_hex[65]) {
FILE *f = fopen(path, "rb");
unsigned char hash[32];
unsigned char buf[4096];
size_t n;
SHA256_CTX ctx;
if (!f) {
return -1;
}
sha256_init(&ctx);
while ((n = fread(buf, 1, sizeof(buf), f)) > 0) {
sha256_update(&ctx, buf, n);
}
fclose(f);
sha256_final(&ctx, hash);
for (int i = 0; i < 32; i++) {
snprintf(out_hex + (i * 2), 3, "%02x", hash[i]);
}
out_hex[64] = '\0';
return 0;
}
+615
View File
@@ -0,0 +1,615 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <time.h>
#include <unistd.h>
#include <sys/stat.h>
#include "sources.h"
#include "repository.h"
#include "sha256.h"
#include "json_helpers.h"
#include "config.h"
#include "pldmgr.h"
#include "payload_mgr.h"
/* ── Source entry type ─────────────────────────────────────── */
typedef struct {
char id[64];
char name[256];
char url[1024];
int removable;
} SourceEntry;
/* ── Load / Save sources from SOURCES_CONFIG_PATH ──────────── */
static int load_sources(SourceEntry *out, int *out_count) {
char *json = NULL;
size_t size = 0;
int count = 0;
memset(out, 0, sizeof(SourceEntry) * MAX_SOURCES);
/* Always start with the default source at index 0 */
strncpy(out[0].id, "default", sizeof(out[0].id) - 1);
strncpy(out[0].name, "Official Repository", sizeof(out[0].name) - 1);
strncpy(out[0].url, REPOSITORY_SOURCE_URL, sizeof(out[0].url) - 1);
out[0].removable = 0;
count = 1;
if (read_file_text(SOURCES_CONFIG_PATH, &json, &size) != 0 || !json || size == 0) {
if (json) free(json);
*out_count = count;
return 0;
}
/* Find "sources": [...] array */
const char *arr_start = strstr(json, "\"sources\"");
if (!arr_start) { free(json); *out_count = count; return 0; }
arr_start = strchr(arr_start, '[');
if (!arr_start) { free(json); *out_count = count; return 0; }
const char *p = arr_start + 1;
while (count < MAX_SOURCES && (p = strchr(p, '{')) != NULL) {
const char *end = strchr(p, '}');
if (!end) break;
char id[64] = "", name[256] = "", url[1024] = "";
char removable_str[8] = "";
json_extract_string(p, end, "id", id, sizeof(id));
json_extract_string(p, end, "name", name, sizeof(name));
json_extract_string(p, end, "url", url, sizeof(url));
json_extract_string(p, end, "removable", removable_str, sizeof(removable_str));
/* Skip the default entry stored in file — we already added it */
if (strcmp(id, "default") == 0) { p = end + 1; continue; }
if (name[0] && url[0] && id[0]) {
strncpy(out[count].id, id, sizeof(out[count].id) - 1);
strncpy(out[count].name, name, sizeof(out[count].name) - 1);
strncpy(out[count].url, url, sizeof(out[count].url) - 1);
out[count].removable = (strcmp(removable_str, "false") != 0);
count++;
}
p = end + 1;
}
free(json);
*out_count = count;
return 0;
}
static int save_sources(SourceEntry *sources, int count) {
char buf[16384];
JsonListBuilder jb = { buf, sizeof(buf), 0, 1 };
ensure_dir_recursive(BASE_DATA_DIR);
json_append(&jb, "{\"sources\":[\n");
for (int i = 0; i < count; i++) {
char id_e[128], name_e[512], url_e[2048];
pldmgr_json_escape(sources[i].id, id_e, sizeof(id_e));
pldmgr_json_escape(sources[i].name, name_e, sizeof(name_e));
pldmgr_json_escape(sources[i].url, url_e, sizeof(url_e));
if (json_append(&jb, " {\"id\":\"%s\",\"name\":\"%s\",\"url\":\"%s\",\"removable\":%s}%s\n",
id_e, name_e, url_e,
sources[i].removable ? "true" : "false",
(i < count - 1) ? "," : "") != 0) {
break;
}
}
json_append(&jb, "]}\n");
return write_file_text(SOURCES_CONFIG_PATH, buf, jb.pos);
}
/* ── Public API ────────────────────────────────────────────── */
int sources_list_json(char *buf, size_t size) {
SourceEntry sources[MAX_SOURCES] = {0};
int count = 0;
load_sources(sources, &count);
JsonListBuilder jb = { buf, size, 0, 1 };
buf[0] = '\0';
json_append(&jb, "{\"sources\":[\n");
for (int i = 0; i < count; i++) {
char id_e[128], name_e[512], url_e[2048];
pldmgr_json_escape(sources[i].id, id_e, sizeof(id_e));
pldmgr_json_escape(sources[i].name, name_e, sizeof(name_e));
pldmgr_json_escape(sources[i].url, url_e, sizeof(url_e));
if (json_append(&jb, " {\"id\":\"%s\",\"name\":\"%s\",\"url\":\"%s\",\"removable\":%s}%s\n",
id_e, name_e, url_e,
sources[i].removable ? "true" : "false",
(i < count - 1) ? "," : "") != 0) {
break;
}
}
json_append(&jb, "]}\n");
return 0;
}
int sources_save(const char *json, size_t len) {
SourceEntry sources[MAX_SOURCES] = {0};
int count = 0;
if (!json || len == 0) return -1;
/* Always put default at slot 0 */
strncpy(sources[0].id, "default", sizeof(sources[0].id) - 1);
strncpy(sources[0].name, "Official Repository", sizeof(sources[0].name) - 1);
strncpy(sources[0].url, REPOSITORY_SOURCE_URL, sizeof(sources[0].url) - 1);
sources[0].removable = 0;
count = 1;
const char *arr_start = strstr(json, "\"sources\"");
if (!arr_start) return -1;
arr_start = strchr(arr_start, '[');
if (!arr_start) return -1;
const char *p = arr_start + 1;
while (count < MAX_SOURCES && (p = strchr(p, '{')) != NULL) {
const char *end = strchr(p, '}');
if (!end) break;
char id[64] = "", name[256] = "", url[1024] = "";
json_extract_string(p, end, "id", id, sizeof(id));
json_extract_string(p, end, "name", name, sizeof(name));
json_extract_string(p, end, "url", url, sizeof(url));
if (strcmp(id, "default") == 0) { p = end + 1; continue; }
if (!name[0] || !url[0] || !id[0]) { p = end + 1; continue; }
strncpy(sources[count].id, id, sizeof(sources[count].id) - 1);
strncpy(sources[count].name, name, sizeof(sources[count].name) - 1);
strncpy(sources[count].url, url, sizeof(sources[count].url) - 1);
sources[count].removable = 1;
count++;
p = end + 1;
}
return save_sources(sources, count);
}
int sources_add(const char *url, char *msg_buf, size_t msg_size) {
char tmp_path[512];
char *json = NULL;
size_t json_size = 0;
RepoPayload *items = NULL;
size_t count = 0;
char source_name[256] = "";
if (!url || !url[0]) {
snprintf(msg_buf, msg_size, "URL is empty");
return -1;
}
ensure_dir_recursive(BASE_DATA_DIR);
snprintf(tmp_path, sizeof(tmp_path), "%s/source_validate.tmp", BASE_DATA_DIR);
if (download_to_file(url, tmp_path) != 0) {
snprintf(msg_buf, msg_size, "URL unreachable or download failed");
remove(tmp_path);
return -1;
}
if (read_file_text(tmp_path, &json, &json_size) != 0 || !json || json_size == 0) {
snprintf(msg_buf, msg_size, "Failed to read downloaded JSON");
remove(tmp_path);
if (json) free(json);
return -1;
}
remove(tmp_path);
/* Read top-level "name" field.
* Only search BEFORE the "payloads" key to avoid picking up individual
* payload items' own "name" fields. */
{
const char *payloads_key = strstr(json, "\"payloads\"");
if (payloads_key) {
size_t search_len = (size_t)(payloads_key - json);
const char *name_key = NULL;
for (size_t i = 0; i + 6 <= search_len; i++) {
if (memcmp(json + i, "\"name\"", 6) == 0) {
name_key = json + i;
break;
}
}
if (name_key) {
const char *colon = strchr(name_key + 6, ':');
if (colon && (size_t)(colon - json) < search_len) {
const char *q = colon + 1;
while (*q == ' ' || *q == '\t') q++;
if (*q == '"') {
q++;
size_t pos = 0;
while (*q && *q != '"' && pos < sizeof(source_name) - 1)
source_name[pos++] = *q++;
source_name[pos] = '\0';
}
}
}
}
/* If still empty, fall back to the hostname portion of the URL */
if (!source_name[0]) {
const char *host = strstr(url, "://");
host = host ? host + 3 : url;
size_t pos = 0;
while (*host && *host != '/' && *host != ':' &&
pos < sizeof(source_name) - 1)
source_name[pos++] = *host++;
source_name[pos] = '\0';
}
}
if (parse_repository_payloads(json, &items, &count) != 0 || count == 0) {
free(json);
if (items) free(items);
snprintf(msg_buf, msg_size, "No valid payloads found in source JSON");
return -1;
}
free(items);
free(json);
/* Load current sources and append */
SourceEntry sources[MAX_SOURCES] = {0};
int src_count = 0;
load_sources(sources, &src_count);
if (src_count >= MAX_SOURCES) {
snprintf(msg_buf, msg_size, "Maximum number of sources (%d) reached", MAX_SOURCES);
return -1;
}
/* Check for duplicate URL */
for (int i = 0; i < src_count; i++) {
if (strcmp(sources[i].url, url) == 0) {
snprintf(msg_buf, msg_size, "Source already added");
return -1;
}
}
/* Assign a unique ID */
char new_id[64];
snprintf(new_id, sizeof(new_id), "source_%d", (int)time(NULL));
strncpy(sources[src_count].id, new_id, sizeof(sources[src_count].id) - 1);
strncpy(sources[src_count].name, source_name, sizeof(sources[src_count].name) - 1);
strncpy(sources[src_count].url, url, sizeof(sources[src_count].url) - 1);
sources[src_count].removable = 1;
src_count++;
if (save_sources(sources, src_count) != 0) {
snprintf(msg_buf, msg_size, "Failed to save sources");
return -1;
}
/* Return the source name in msg_buf for the caller to forward to the UI */
snprintf(msg_buf, msg_size, "%s", source_name);
return 0;
}
int sources_remove(int index, char *msg_buf, size_t msg_size) {
if (index <= 0) {
snprintf(msg_buf, msg_size, "Cannot remove the default source");
return -1;
}
SourceEntry sources[MAX_SOURCES] = {0};
int count = 0;
load_sources(sources, &count);
if (index >= count) {
snprintf(msg_buf, msg_size, "Invalid source index");
return -1;
}
/* Shift entries down */
for (int i = index; i < count - 1; i++) {
sources[i] = sources[i + 1];
}
count--;
if (save_sources(sources, count) != 0) {
snprintf(msg_buf, msg_size, "Failed to save sources");
return -1;
}
snprintf(msg_buf, msg_size, "OK");
return 0;
}
/* ── Multi-source repository ───────────────────────────────── */
static void get_source_cache_path(int index, char *out, size_t out_size) {
if (index == 0) {
strncpy(out, REPOSITORY_CACHE_PATH, out_size - 1);
out[out_size - 1] = '\0';
} else {
snprintf(out, out_size, "%s.src%d.json", REPOSITORY_CACHE_PATH, index);
}
}
static int ensure_source_fresh(const char *url, const char *cache_path, int force_refresh) {
long last_update = 0;
time_t now = time(NULL);
char tmp_path[512];
char *json = NULL;
size_t json_size = 0;
RepoPayload *items = NULL;
size_t count = 0;
ensure_dir_recursive(BASE_DATA_DIR);
/* Read last update time from the cache file's companion .ts file */
char ts_path[512];
snprintf(ts_path, sizeof(ts_path), "%s.ts", cache_path);
FILE *tf = fopen(ts_path, "r");
if (tf) {
fscanf(tf, "%ld", &last_update);
fclose(tf);
}
if (!force_refresh && access(cache_path, F_OK) == 0) {
long delta = (long)now - last_update;
if (delta >= 0 && delta < REPOSITORY_REFRESH_INTERVAL_SEC) {
return 0;
}
}
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", cache_path);
if (download_to_file(url, tmp_path) != 0) {
return -1;
}
if (read_file_text(tmp_path, &json, &json_size) != 0 || json_size == 0) {
if (json) free(json);
remove(tmp_path);
return -1;
}
/* Validate: must have at least 1 payload */
if (parse_repository_payloads(json, &items, &count) != 0 || count == 0) {
free(items);
free(json);
remove(tmp_path);
return -1;
}
free(items);
free(json);
if (rename(tmp_path, cache_path) != 0) {
remove(tmp_path);
return -1;
}
/* Write timestamp */
tf = fopen(ts_path, "w");
if (tf) {
fprintf(tf, "%ld", (long)now);
fclose(tf);
}
return 0;
}
size_t sources_multi_repository_list_json(char *buf, size_t size, int force_refresh) {
SourceEntry sources[MAX_SOURCES] = {0};
int src_count = 0;
load_sources(sources, &src_count);
JsonListBuilder jb = { buf, size, 0, 1 };
buf[0] = '\0';
json_append(&jb, "{\"sources\":[\n");
for (int si = 0; si < src_count; si++) {
char cache_path[512];
get_source_cache_path(si, cache_path, sizeof(cache_path));
int fetch_ok = (ensure_source_fresh(sources[si].url, cache_path, force_refresh) == 0);
char id_e[128], name_e[512];
pldmgr_json_escape(sources[si].id, id_e, sizeof(id_e));
pldmgr_json_escape(sources[si].name, name_e, sizeof(name_e));
/* Read last_update timestamp */
long last_update = 0;
char ts_path[512];
snprintf(ts_path, sizeof(ts_path), "%s.ts", cache_path);
FILE *tf = fopen(ts_path, "r");
if (tf) { fscanf(tf, "%ld", &last_update); fclose(tf); }
if (json_append(&jb, " {\"id\":\"%s\",\"name\":\"%s\",\"last_update\":%ld,\"error\":%s,\"payloads\":[\n",
id_e, name_e, last_update, fetch_ok ? "false" : "true") != 0) {
break;
}
if (fetch_ok) {
char *cached = NULL;
size_t cached_size = 0;
RepoPayload *items = NULL;
size_t count = 0;
if (read_file_text(cache_path, &cached, &cached_size) == 0 && cached) {
parse_repository_payloads(cached, &items, &count);
free(cached);
}
for (size_t pi = 0; pi < count; pi++) {
char name_pe[256], filename_e[512], desc_e[2048];
char ver_e[128], url_e[2048], src_id_e[128], src_name_e[512];
pldmgr_json_escape(items[pi].name, name_pe, sizeof(name_pe));
pldmgr_json_escape(items[pi].filename, filename_e, sizeof(filename_e));
pldmgr_json_escape(items[pi].description, desc_e, sizeof(desc_e));
pldmgr_json_escape(items[pi].version, ver_e, sizeof(ver_e));
pldmgr_json_escape(items[pi].url, url_e, sizeof(url_e));
pldmgr_json_escape(sources[si].id, src_id_e, sizeof(src_id_e));
pldmgr_json_escape(sources[si].name, src_name_e, sizeof(src_name_e));
if (json_append(&jb, " {\"name\":\"%s\",\"filename\":\"%s\",\"description\":\"%s\","
"\"version\":\"%s\",\"url\":\"%s\","
"\"source_id\":\"%s\",\"source_name\":\"%s\"}%s\n",
name_pe, filename_e, desc_e, ver_e, url_e,
src_id_e, src_name_e,
(pi < count - 1) ? "," : "") != 0) {
break;
}
}
if (items) free(items);
}
if (json_append(&jb, " ]}%s\n", (si < src_count - 1) ? "," : "") != 0) {
break;
}
}
json_append(&jb, "]}\n");
return jb.pos;
}
int sources_multi_repository_install(const char *filename, const char *source_id,
const char *repo_url, char *msg, size_t msg_size) {
SourceEntry sources[MAX_SOURCES] = {0};
int src_count = 0;
load_sources(sources, &src_count);
/* Find the matching source */
int si = -1;
for (int i = 0; i < src_count; i++) {
if (strcmp(sources[i].id, source_id) == 0) { si = i; break; }
}
if (si < 0) {
/* Fall back to matching by URL */
for (int i = 0; i < src_count; i++) {
if (repo_url && strcmp(sources[i].url, repo_url) == 0) { si = i; break; }
}
}
if (si < 0) {
/* Last resort: use default */
si = 0;
}
char cache_path[512];
get_source_cache_path(si, cache_path, sizeof(cache_path));
/* Ensure cache is fresh */
if (access(cache_path, F_OK) != 0) {
if (ensure_source_fresh(sources[si].url, cache_path, 1) != 0) {
snprintf(msg, msg_size, "Failed to fetch source repository");
return -1;
}
}
/* Load cached payloads for this source */
char *cached = NULL;
size_t cached_size = 0;
RepoPayload *items = NULL;
size_t count = 0;
if (read_file_text(cache_path, &cached, &cached_size) != 0 || !cached) {
snprintf(msg, msg_size, "Repository cache unavailable");
return -1;
}
parse_repository_payloads(cached, &items, &count);
free(cached);
/* Find the requested payload */
int found = -1;
for (size_t i = 0; i < count; i++) {
if (strcmp(items[i].filename, filename) == 0) { found = (int)i; break; }
}
if (found < 0) {
if (items) free(items);
snprintf(msg, msg_size, "Payload not found in source");
return -1;
}
/* Download */
ensure_dir_recursive(PAYLOADS_STORAGE_DIR);
char tmp_path[512];
snprintf(tmp_path, sizeof(tmp_path), "%s/%s.part", PAYLOADS_STORAGE_DIR, items[found].filename);
if (download_to_file(items[found].url, tmp_path) != 0) {
if (items) free(items);
remove(tmp_path);
snprintf(msg, msg_size, "Download failed");
return -1;
}
/* Commit */
const char *detail = (repo_url && repo_url[0]) ? repo_url : sources[si].url;
char folder_name[128];
char payload_dir[512];
char final_path[640];
char details_path[700];
pldmgr_utils_get_payload_folder_name(items[found].filename, folder_name, sizeof(folder_name));
snprintf(payload_dir, sizeof(payload_dir), "%s/%s", PAYLOADS_STORAGE_DIR, folder_name);
if (ensure_dir_recursive(payload_dir) != 0) {
if (items) free(items);
remove(tmp_path);
snprintf(msg, msg_size, "Failed to create payload directory");
return -1;
}
snprintf(final_path, sizeof(final_path), "%s/%s", payload_dir, items[found].filename);
snprintf(details_path, sizeof(details_path), "%s/%s.json", payload_dir, items[found].filename);
/* Checksum verification (optional — skip if checksum absent) */
if (strlen(items[found].checksum) == 64) {
char calculated[65];
if (compute_sha256_file(tmp_path, calculated) != 0) {
if (items) free(items);
remove(tmp_path);
snprintf(msg, msg_size, "Checksum computation failed");
return -1;
}
if (strcasecmp(calculated, items[found].checksum) != 0) {
pldmgr_log("[PLDMGR] !!! Checksum mismatch for %s\n", items[found].filename);
if (items) free(items);
remove(tmp_path);
snprintf(msg, msg_size, "Checksum mismatch");
return -1;
}
}
payload_mgr_remove_old_files(payload_dir, items[found].filename);
if (rename(tmp_path, final_path) != 0) {
if (items) free(items);
remove(tmp_path);
snprintf(msg, msg_size, "Failed to finalize payload file");
return -1;
}
/* Attach source_id and source_name to the item so metadata captures them */
strncpy(items[found].source, sources[si].id, sizeof(items[found].source) - 1);
strncpy(items[found].source_direct, sources[si].name, sizeof(items[found].source_direct) - 1);
strncpy(items[found].source_name, sources[si].name, sizeof(items[found].source_name) - 1);
if (write_payload_details_json(&items[found], details_path, "repository", detail) != 0) {
if (items) free(items);
snprintf(msg, msg_size, "Failed to write payload metadata");
return -1;
}
pldmgr_log("[PLDMGR] Multi-source install complete: %s (source: %s)\n",
items[found].filename, sources[si].name);
snprintf(msg, msg_size, "Installed %s", items[found].filename);
if (items) free(items);
return 0;
}