fix: validate partial settings and synchronize persistent edits and history

This commit is contained in:
najdek committed 2026-10-12 23:23:19 +02:00
1 parent 1ee6a61bc2
commit ab82dbf05e
24 files changed
+873 -636

No files matched your search

+7
View File
@@ -18,6 +18,13 @@ jobs:
- name: Test backend JSON and HTTP with sanitizers and real sockets
run: docker run --rm --network=none -v "$GITHUB_WORKSPACE:/src:ro" pldmgr-tests make test-host
shared-state:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: Check concurrent config, sources and history with ThreadSanitizer
run: python3 tests/run_config_tests.py --thread-sanitize
frontend:
runs-on: ubuntu-latest
steps:
+16 -3
View File
@@ -131,6 +131,19 @@ cross-origin GET compatibility. The verification workflow also runs actual Chrom
against a native stub server and Vite, checking hostile requests, real pagehide
beacons, and zero rejected-request effects. Run `node tests/run_browser_mutation_tests.mjs` for these checks.
Settings now accept validated partial updates. Saves atomically replace individual
files, preserve unspecified fields/timestamps, and return errors on failed writes.
The UI sends only the fields it edits. Settings/favorites/autoload remaps and
source edits serialize read-modify-write; history reads serialize a snapshot of
the 20 most recent entries. The new `run_config_tests.py` covers actual persistence,
injected I/O failures and concurrent history/settings/source operations.
Run `python3 tests/run_config_tests.py --thread-sanitize` on a host supporting
Clang ThreadSanitizer; local macOS verification passed and CI has a macOS job.
A multi-file settings request validates fully first, publishes lists before scalar
settings, and reports 500 if any publication fails. Earlier completed files stay
published; per-file atomicity does not provide an all-or-nothing transaction.
Collected arrays are limited to **4,096 items** and **64 MiB** of allocated array
capacity, whichever is smaller for the item type. Repository parsing accepts up
to **16 MiB** of input text. These bounds apply to the parser/scanner collections;
@@ -141,8 +154,8 @@ Collection failures become explicit errors rather than successful partial lists.
and frontend build without publishing or deploying. Frontend lint still reports existing errors. The host suite tests response
ownership/status handling, request cleanup, and autoload-list bounds. Storage
transactions, autoload worker concurrency, and PS5 firmware behavior remain
separate validation scopes. Backend storage/service calls in the HTTP tests are
stubbed; successful mock imports move actual files inside temporary fixtures.
separate validation scopes. The main HTTP fixture stubs backend storage/services; the separate
`--config-only` HTTP fixture uses actual config persistence. Successful mock imports move actual files inside temporary fixtures.
The HTTP server accepts config/source bodies up to **1 MiB** and streamed uploads
up to **512 MiB**. Larger declared or accumulated bodies return HTTP 413; empty
@@ -169,7 +182,7 @@ build a candidate under `scratch/` rather than run `make clean`:
```bash
mkdir -p scratch
docker run --rm --network=none -v "$(pwd):/src" -w /src \
ps5-payload-sdk-pldmgr make ELF=scratch/pldmgr-candidate.elf
ps5-payload-sdk-pldmgr make ELF=scratch/pldmgr-step5.elf
```
This command assumes the SDK image, frontend distribution, and embedded CA
+2
View File
@@ -57,6 +57,8 @@ test-host:
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_allocation_tests.py
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_integrity_tests.py
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_storage_tests.py
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_config_tests.py
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_http_tests.py --config-only
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_http_tests.py
test-frontend:
+1 -2
View File
@@ -579,11 +579,10 @@ function App() {
}
const handleSaveConfig = async (newConfig) => {
const merged = { ...config, ...newConfig }
const success = await api('/set_config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(merged),
body: JSON.stringify(newConfig),
keepalive: true,
})
if (success) {
@@ -98,7 +98,6 @@ const AutoloadView = ({
lastSyncedRef.current = syncKey
try {
const body = JSON.stringify({
...config,
AUTOLOAD_ENABLED: en,
AUTOLOAD_LIST: finalStr,
})
@@ -44,7 +44,7 @@ const SettingsView = ({ config, onSaveConfig, setShowLogs, onNavigate }) => {
const { t, i18n } = useTranslation()
const autoOpen = config.AUTO_BROWSER_OPEN !== false
const autoInstall = config.AUTO_INSTALL_APP !== false
const autoloadDelay = config.AUTOLOAD_DELAY || 5
const autoloadDelay = config.AUTOLOAD_DELAY ?? 5
const multiSources = config.MULTI_SOURCES_ENABLED === true
const [followBrowserLanguage, setFollowBrowserLanguage] = useState(isFollowingBrowserLanguage)
+1 -1
View File
@@ -10,4 +10,4 @@ void pldmgr_autoload_reset();
int pldmgr_autoload_get_remaining_seconds();
long long pldmgr_autoload_get_remaining_ms();
void pldmgr_autoload_get_status(int *total, int *done, char *current);
void pldmgr_autoload_update_config_entry(const char *old_filename, const char *new_filename);
int pldmgr_autoload_update_config_entry(const char *old_filename, const char *new_filename);
+11 -3
View File
@@ -26,10 +26,15 @@ int config_write(const PldmgrConfig *cfg);
* Returns default_val if the key or file is missing. */
int config_read_bool(const char *key, int default_val);
enum { CONFIG_INVALID = -2 };
/* Apply a JSON config update sent from the frontend /set_config route.
* Handles AUTOLOAD_ENABLED, AUTO_BROWSER_OPEN, AUTOLOAD_DELAY,
* SCAN_USB_PAYLOADS, AUTO_INSTALL_APP, MULTI_SOURCES_ENABLED, and AUTOLOAD_LIST.
* Returns 0 on success, -1 on error. */
* Booleans must be JSON booleans; delay is an integer from 0 to 3600 seconds.
* Returns 0, CONFIG_INVALID for bad input, or -1 for persistence failure.
* All input is validated first; each file publishes atomically, but a multi-file
* I/O failure may retain earlier publications. */
int config_handle_set_json(const char *json_data);
/* Upsert a single key=value line in the config file.
@@ -37,15 +42,18 @@ int config_handle_set_json(const char *json_data);
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. */
* Sets *out_ts to 0 if missing; returns -1 on read failure. */
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);
/* Serialized, atomic replacement/removal of matching list entries. */
int config_update_list_entry(const char *path, const char *old_key, const char *new_key);
/* Favorites management */
int config_get_favorites_csv(char *out_buf, size_t max_size, int *out_file_exists);
int config_save_favorites_csv(const char *csv);
void pldmgr_favorites_update_entry(const char *old_key, const char *new_key);
int pldmgr_favorites_update_entry(const char *old_key, const char *new_key);
#endif
+14 -1
View File
@@ -46,11 +46,24 @@ int json_extract_string_prefix(const char *obj_start, const char *obj_end,
* outside [start, limit); allows following array members after the object. */
int json_find_object_end(const char *start, const char *limit, const char **end);
/* Visit direct members/elements of a complete, validated object/array.
* Spans exclude surrounding whitespace. Callback returns 0 or -1.
* Object keys are decoded into a bounded C string (maximum 255 bytes). */
typedef int (*JsonMemberVisitor)(const char *key, const char *start,
const char *end, void *context);
typedef int (*JsonValueVisitor)(const char *start, const char *end, void *context);
int json_visit_object(const char *start, const char *end, JsonMemberVisitor visit, void *context);
int json_visit_array(const char *start, const char *end, JsonValueVisitor visit, void *context);
int json_decode_string(const char *start, const char *end, char *out, size_t capacity);
/* 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. */
/* Atomically replace one file via a unique sibling temporary, checked write,
* flush/file sync/close and rename. Before publication failure retains the old
* file. Returns 0 once published, -1 on failure. Directory sync is best-effort;
* this does not promise a transaction across files or power-loss recovery. */
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. */
+5 -41
View File
@@ -18,6 +18,7 @@ long long get_current_time_ms() {
#include "pldmgr.h"
#include "autoload.h"
#include "config.h"
#include "payload_mgr.h"
#include "slot.h"
#include "ps5_launcher.h"
@@ -251,45 +252,8 @@ void pldmgr_autoload_reset() {
strcpy(autoload_current_name, "");
}
void pldmgr_autoload_update_config_entry(const char *old_filename, const char *new_filename) {
FILE *f = fopen(AUTOLOAD_CONFIG_PATH, "r");
if (!f) return;
char lines[100][256];
int line_count = 0;
int modified = 0;
while (line_count < 100 && fgets(lines[line_count], sizeof(lines[0]), f)) {
char clean_line[256];
strcpy(clean_line, lines[line_count]);
clean_line[strcspn(clean_line, "\r\n")] = 0;
char *start = clean_line;
while (*start && isspace((unsigned char)*start)) start++;
char *end = start + strlen(start);
while (end > start && isspace((unsigned char)*(end - 1))) end--;
*end = '\0';
if (strcmp(start, old_filename) == 0) {
modified = 1;
if (new_filename) {
snprintf(lines[line_count], sizeof(lines[0]), "%s\n", new_filename);
line_count++;
}
/* If new_filename is NULL, we just skip incrementing line_count, effectively deleting it */
} else {
line_count++;
}
}
fclose(f);
if (modified) {
f = fopen(AUTOLOAD_CONFIG_PATH, "w");
if (f) {
for (int i = 0; i < line_count; i++) {
fputs(lines[i], f);
}
fclose(f);
pldmgr_log("[Autoload] Config updated: replaced/removed %s\n", old_filename);
}
}
int pldmgr_autoload_update_config_entry(const char *old_filename, const char *new_filename) {
int result = config_update_list_entry(AUTOLOAD_CONFIG_PATH, old_filename, new_filename);
if (result) pldmgr_log("[Autoload] Failed to update entry: %s\n", old_filename ? old_filename : "");
return result;
}
+229 -328
View File
@@ -3,292 +3,228 @@
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <sys/stat.h>
#include <limits.h>
#include <pthread.h>
#include "config.h"
#include "pldmgr.h"
#include "json_helpers.h"
/* ── Read / Write full config ──────────────────────────────── */
/* Covers read/modify/write across settings, favorites and autoload edits.
* Never call loader/autoload callbacks with this lock held. */
static pthread_mutex_t config_mutex = PTHREAD_MUTEX_INITIALIZER;
#define LIST_CAPACITY 4096
#define CONFIG_LIMIT (64 * 1024)
void config_read(PldmgrConfig *cfg) {
FILE *f;
char line[256];
static const char *setting_keys[] = {"AUTOLOAD_ENABLED", "AUTO_BROWSER_OPEN",
"AUTOLOAD_DELAY", "SCAN_USB_PAYLOADS", "AUTO_INSTALL_APP", "MULTI_SOURCES_ENABLED"};
/* Defaults */
cfg->autoload_enabled = 0;
cfg->last_repository_update = 0;
cfg->auto_browser_open = 1;
cfg->autoload_delay = 5;
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, "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);
static void defaults(PldmgrConfig *cfg) {
*cfg = (PldmgrConfig){.auto_browser_open=1, .autoload_delay=5, .auto_install_app=1};
}
static int read_config(PldmgrConfig *cfg) {
defaults(cfg);
char *text = NULL; size_t size = 0;
if (read_file_text(PLDMGR_CONFIG_PATH, &text, &size)) return errno == ENOENT ? 0 : -1;
if (size > CONFIG_LIMIT || memchr(text, 0, size)) { free(text); return -1; }
char *save = NULL;
for (char *line = strtok_r(text, "\r\n", &save); line; line = strtok_r(NULL, "\r\n", &save)) {
char *equal = strchr(line, '=');
if (!equal) continue;
*equal++ = 0;
char *end; errno = 0;
long value = strtol(equal, &end, 10);
if (errno || *end || end == equal) continue;
if (!strcmp(line, "LAST_REPOSITORY_UPDATE")) { if (value >= 0) cfg->last_repository_update = value; continue; }
int *fields[] = {&cfg->autoload_enabled, &cfg->auto_browser_open, &cfg->autoload_delay,
&cfg->scan_usb_payloads, &cfg->auto_install_app, &cfg->multi_sources_enabled};
for (size_t i = 0; i < 6; i++) if (!strcmp(line, setting_keys[i])) {
if (i == 2 ? value >= 0 && value <= 3600 : value == 0 || value == 1) *fields[i] = (int)value;
}
}
fclose(f);
free(text); return 0;
}
void config_read(PldmgrConfig *cfg) { if (cfg) read_config(cfg); }
static int write_config(const PldmgrConfig *cfg) {
char text[512];
int length = snprintf(text, sizeof(text),
"AUTOLOAD_ENABLED=%d\nLAST_REPOSITORY_UPDATE=%ld\nAUTO_BROWSER_OPEN=%d\n"
"AUTOLOAD_DELAY=%d\nSCAN_USB_PAYLOADS=%d\nAUTO_INSTALL_APP=%d\nMULTI_SOURCES_ENABLED=%d\n",
!!cfg->autoload_enabled, cfg->last_repository_update, !!cfg->auto_browser_open,
cfg->autoload_delay, !!cfg->scan_usb_payloads, !!cfg->auto_install_app, !!cfg->multi_sources_enabled);
if (length < 0 || (size_t)length >= sizeof(text) || ensure_dir_recursive(BASE_DATA_DIR)) return -1;
char *old = NULL; size_t size = 0;
if (read_file_text(PLDMGR_CONFIG_PATH, &old, &size) && errno != ENOENT) return -1;
if (size > CONFIG_LIMIT || (old && memchr(old, 0, size))) { free(old); return -1; }
char *complete = malloc((size_t)length + size + 2);
if (!complete) { free(old); return -1; }
memcpy(complete, text, (size_t)length);
size_t pos = (size_t)length;
const char *line = old;
while (line && line < old + size) {
const char *end = memchr(line, '\n', (size_t)(old + size - line));
if (!end) end = old + size;
size_t n = (size_t)(end - line);
int known = n >= 23 && !memcmp(line, "LAST_REPOSITORY_UPDATE=", 23);
for (size_t i = 0; i < 6; i++) {
size_t k = strlen(setting_keys[i]);
if (n > k && !memcmp(line, setting_keys[i], k) && line[k] == '=') known = 1;
}
if (!known) { memcpy(complete + pos, line, n); pos += n; complete[pos++] = '\n'; }
line = end < old + size ? end + 1 : old + size;
}
int result = pos > CONFIG_LIMIT ? -1 : write_file_text(PLDMGR_CONFIG_PATH, complete, pos);
free(complete); free(old); return result;
}
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, "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;
if (!cfg || cfg->autoload_delay < 0 || cfg->autoload_delay > 3600 || cfg->last_repository_update < 0) return -1;
pthread_mutex_lock(&config_mutex);
int result = write_config(cfg);
pthread_mutex_unlock(&config_mutex); return result;
}
/* ── 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;
if (!key) return default_val;
char *text = NULL; size_t size = 0;
if (read_file_text(PLDMGR_CONFIG_PATH, &text, &size)) return default_val;
int result = default_val;
if (size <= CONFIG_LIMIT && !memchr(text, 0, size)) {
char *save = NULL;
for (char *line = strtok_r(text, "\r\n", &save); line; line = strtok_r(NULL, "\r\n", &save)) {
size_t length = strlen(key);
if (strlen(line) > length && !strncmp(line, key, length) && line[length] == '=') {
if (!strcmp(line + length + 1, "0")) result = 0;
else if (!strcmp(line + length + 1, "1")) result = 1;
break;
}
}
}
fclose(f);
return res;
free(text); return result;
}
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++;
if (!key || !key[0] || !value || strlen(key) > 255 || strlen(value) > CONFIG_LIMIT ||
strpbrk(key, "=\r\n") || strpbrk(value, "\r\n")) return -1;
pthread_mutex_lock(&config_mutex);
char *old = NULL; size_t size = 0;
int result = -1;
if (read_file_text(PLDMGR_CONFIG_PATH, &old, &size) && errno != ENOENT) goto done;
if (size > CONFIG_LIMIT || (old && memchr(old, 0, size))) goto done;
size_t capacity = size + strlen(key) + strlen(value) + 4;
char *text = malloc(capacity);
if (!text) goto done;
size_t pos = 0;
/* Preserve unrelated and unknown keys without fixed-line truncation. */
const char *line = old;
while (line && line < old + size) {
const char *end = memchr(line, '\n', (size_t)(old + size - line));
if (!end) end = old + size;
size_t n = (size_t)(end - line), key_size = strlen(key);
if (!(n > key_size && !memcmp(line, key, key_size) && line[key_size] == '=')) {
memcpy(text + pos, line, n); pos += n; text[pos++] = '\n';
}
fclose(f);
line = end < old + size ? end + 1 : old + size;
}
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 count = snprintf(text + pos, capacity - pos, "%s=%s\n", key, value);
if (count >= 0 && (size_t)count < capacity - pos && !ensure_dir_recursive(BASE_DATA_DIR))
result = write_file_text(PLDMGR_CONFIG_PATH, text, pos + (size_t)count);
free(text);
done:
free(old); pthread_mutex_unlock(&config_mutex); return result;
}
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;
if (!out_ts) return -1;
PldmgrConfig cfg; int result = read_config(&cfg); *out_ts = cfg.last_repository_update; return result;
}
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);
if (ts < 0) return -1;
char value[64]; snprintf(value, sizeof(value), "%ld", ts);
return config_upsert_value("LAST_REPOSITORY_UPDATE", value);
}
/* ── 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;
/* CSV is a legacy wire format; commas separate entries, never occur inside one.
* Newlines/control characters cannot be represented safely in line files. */
static int csv_lines(const char *csv, char *out, size_t capacity) {
size_t pos = 0;
if (!csv) csv = "";
const char *start = csv;
while (*start) {
const char *end = strchr(start, ','); if (!end) end = start + strlen(start);
const char *next = *end ? end + 1 : end;
while (start < end && (*start == ' ' || *start == '\t')) start++;
while (end > start && (end[-1] == ' ' || end[-1] == '\t')) end--;
size_t length = (size_t)(end - start);
for (const char *p = start; p < end; p++) if ((unsigned char)*p < 32 || *p == 127) return -1;
if (length) {
if (length + 1 >= capacity - pos) return -1;
memcpy(out + pos, start, length); pos += length; out[pos++] = '\n';
}
start = next;
}
out[pos] = 0; return 0;
}
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);
static int save_csv(const char *path, const char *csv) {
char lines[LIST_CAPACITY];
if (csv_lines(csv, lines, sizeof(lines)) || ensure_dir_recursive(BASE_DATA_DIR)) return -1;
return write_file_text(path, lines, strlen(lines));
}
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.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);
}
}
}
/* Handle FAVORITES if present */
char *fav_start = strstr(json_data, "\"FAVORITES\"");
if (fav_start) {
char *val = strchr(fav_start, ':');
if (val) {
val++;
while (*val == ' ') val++;
if (*val == '\"') {
val++;
char *fav_end = strchr(val, '\"');
size_t fav_len = fav_end ? (size_t)(fav_end - val) : 0;
char *fav_val = malloc(fav_len + 1);
if (fav_val) {
memcpy(fav_val, val, fav_len);
fav_val[fav_len] = '\0';
config_save_favorites_csv(fav_val);
free(fav_val);
}
} else if (*val == '[') {
val++;
char fav_buf[4096] = {0};
int first = 1;
while (*val && *val != ']') {
char *q1 = strchr(val, '\"');
if (!q1) break;
char *q2 = strchr(q1 + 1, '\"');
if (!q2) break;
size_t item_len = (size_t)(q2 - (q1 + 1));
if (item_len > 0 && item_len < 256) {
char item[256];
memcpy(item, q1 + 1, item_len);
item[item_len] = '\0';
if (!first) strncat(fav_buf, ",", sizeof(fav_buf) - strlen(fav_buf) - 1);
strncat(fav_buf, item, sizeof(fav_buf) - strlen(fav_buf) - 1);
first = 0;
}
val = q2 + 1;
}
config_save_favorites_csv(fav_buf);
}
}
}
typedef struct { unsigned seen; int values[6]; char autoload[LIST_CAPACITY], favorites[LIST_CAPACITY]; } Update;
static int favorite_item(const char *start, const char *end, void *context) {
Update *update = context;
char item[LIST_CAPACITY];
if (json_decode_string(start, end, item, sizeof(item)) || strchr(item, ',') || strpbrk(item, "\r\n")) return -1;
size_t used = strlen(update->favorites), count = strlen(item);
if (used + count + (used && count ? 1 : 0) >= sizeof(update->favorites)) return -1;
if (count) { if (used) update->favorites[used++] = ','; memcpy(update->favorites + used, item, count + 1); }
return 0;
}
/* ── Favorites Management ──────────────────────────────────── */
static int update_member(const char *key, const char *start, const char *end, void *context) {
Update *update = context;
size_t index = 0;
for (; index < 6; index++) if (!strcmp(key, setting_keys[index])) break;
if (!strcmp(key, "AUTOLOAD_LIST")) index = 6;
else if (!strcmp(key, "FAVORITES")) index = 7;
if (index == 6 && strcmp(key, "AUTOLOAD_LIST")) return 0; /* read-only/unknown fields */
unsigned bit = 1u << index;
if (update->seen & bit) return -1;
update->seen |= bit;
size_t size = (size_t)(end - start);
if (index == 2) {
if (!size || size > 4) return -1;
long value = 0;
for (const char *p = start; p < end; p++) { if (*p < '0' || *p > '9') return -1; value = value * 10 + *p - '0'; }
if (value > 3600) return -1;
update->values[index] = (int)value;
} else if (index < 6) {
if (size == 4 && !memcmp(start, "true", 4)) update->values[index] = 1;
else if (size == 5 && !memcmp(start, "false", 5)) update->values[index] = 0;
else return -1;
} else {
char *csv = index == 6 ? update->autoload : update->favorites;
if (index == 7 && *start == '[') {
if (json_visit_array(start, end, favorite_item, update)) return -1;
} else if (json_decode_string(start, end, csv, LIST_CAPACITY)) return -1;
char lines[LIST_CAPACITY];
if (csv_lines(csv, lines, sizeof(lines))) return -1;
}
return 0;
}
int config_handle_set_json(const char *json) {
Update update = {0};
if (!json || json_visit_object(json, json + strlen(json), update_member, &update)) return CONFIG_INVALID;
pthread_mutex_lock(&config_mutex);
PldmgrConfig cfg;
int result = read_config(&cfg);
int *fields[] = {&cfg.autoload_enabled, &cfg.auto_browser_open, &cfg.autoload_delay,
&cfg.scan_usb_payloads, &cfg.auto_install_app, &cfg.multi_sources_enabled};
for (size_t i = 0; i < 6; i++) if (update.seen & (1u << i)) *fields[i] = update.values[i];
/* Validate the entire request before any write. Individual files publish
* atomically; on I/O failure prior successful file publications remain.
* Lists go first, enabled/settings last: never enable an unsaved new list. */
if (!result && (update.seen & (1u << 6))) result = save_csv(AUTOLOAD_CONFIG_PATH, update.autoload);
if (!result && (update.seen & (1u << 7))) result = save_csv(FAVORITES_CONFIG_PATH, update.favorites);
if (!result && (update.seen & 63)) result = write_config(&cfg);
pthread_mutex_unlock(&config_mutex);
if (!result && (update.seen & 1) && !update.values[0]) pldmgr_autoload_abort();
return result;
}
int config_get_favorites_csv(char *out_buf, size_t max_size, int *out_file_exists) {
if (!out_buf || !max_size) return -1;
@@ -340,76 +276,41 @@ failed:
return -1;
}
int config_save_favorites_csv(const char *csv) {
ensure_dir_recursive(BASE_DATA_DIR);
FILE *f = fopen(FAVORITES_CONFIG_PATH, "w");
if (!f) return -1;
if (csv && csv[0] != '\0') {
char *copy = strdup(csv);
if (copy) {
char *token = strtok(copy, ",");
while (token) {
while (*token && isspace((unsigned char)*token)) token++;
char *end = token + strlen(token);
while (end > token && isspace((unsigned char)*(end - 1))) end--;
*end = '\0';
if (*token) {
fprintf(f, "%s\n", token);
}
token = strtok(NULL, ",");
}
free(copy);
}
}
fclose(f);
pldmgr_log("[PLDMGR] Saved favorites to %s\n", FAVORITES_CONFIG_PATH);
return 0;
pthread_mutex_lock(&config_mutex);
int result = save_csv(FAVORITES_CONFIG_PATH, csv);
pthread_mutex_unlock(&config_mutex); return result;
}
void pldmgr_favorites_update_entry(const char *old_key, const char *new_key) {
if (!old_key || old_key[0] == '\0') return;
FILE *f = fopen(FAVORITES_CONFIG_PATH, "r");
if (!f) return;
char lines[100][256];
int line_count = 0;
int modified = 0;
while (line_count < 100 && fgets(lines[line_count], sizeof(lines[0]), f)) {
char clean_line[256];
strncpy(clean_line, lines[line_count], sizeof(clean_line) - 1);
clean_line[sizeof(clean_line) - 1] = '\0';
clean_line[strcspn(clean_line, "\r\n")] = '\0';
char *start = clean_line;
while (*start && isspace((unsigned char)*start)) start++;
char *end = start + strlen(start);
while (end > start && isspace((unsigned char)*(end - 1))) end--;
*end = '\0';
if (strcmp(start, old_key) == 0) {
modified = 1;
if (new_key && new_key[0] != '\0') {
snprintf(lines[line_count], sizeof(lines[0]), "%s\n", new_key);
line_count++;
}
/* If new_key is NULL or empty, skip incrementing line_count -> effectively removed */
} else {
line_count++;
}
}
fclose(f);
if (modified) {
f = fopen(FAVORITES_CONFIG_PATH, "w");
if (f) {
for (int i = 0; i < line_count; i++) {
fputs(lines[i], f);
}
fclose(f);
pldmgr_log("[Favorites] Config updated: replaced/removed %s\n", old_key);
}
int config_update_list_entry(const char *path, const char *old_key, const char *new_key) {
if (!path || !old_key || !old_key[0] || strpbrk(old_key, ",\r\n") ||
strlen(old_key) >= LIST_CAPACITY ||
(new_key && (strlen(new_key) >= LIST_CAPACITY || strpbrk(new_key, ",\r\n")))) return -1;
pthread_mutex_lock(&config_mutex);
char *old = NULL; size_t size = 0; int result = -1;
if (read_file_text(path, &old, &size)) { result = errno == ENOENT ? 0 : -1; goto done; }
if (size > CONFIG_LIMIT || memchr(old, 0, size)) goto done;
size_t capacity = size + 1;
/* Each matching line may grow. Bound allocation even for legacy big files. */
if (new_key) capacity += (size + 1) * (strlen(new_key) + 1);
if (capacity > 16 * 1024 * 1024) goto done;
char *text = malloc(capacity);
if (!text) goto done;
size_t pos = 0; int changed = 0; char *save = NULL;
for (char *line = strtok_r(old, "\r\n", &save); line; line = strtok_r(NULL, "\r\n", &save)) {
while (*line && isspace((unsigned char)*line)) line++;
char *end = line + strlen(line);
while (end > line && isspace((unsigned char)end[-1])) *--end = 0;
if (!strcmp(line, old_key)) { changed = 1; line = (char *)new_key; }
if (line && *line) { size_t n = strlen(line); memcpy(text + pos, line, n); pos += n; text[pos++] = '\n'; }
}
result = changed ? (pos >= LIST_CAPACITY ? -1 : write_file_text(path, text, pos)) : 0;
free(text);
done:
free(old); pthread_mutex_unlock(&config_mutex); return result;
}
int pldmgr_favorites_update_entry(const char *old_key, const char *new_key) {
int result = config_update_list_entry(FAVORITES_CONFIG_PATH, old_key, new_key);
if (result) pldmgr_log("[Favorites] Failed to update entry: %s\n", old_key ? old_key : "");
return result;
}
+12 -3
View File
@@ -3,16 +3,19 @@
#include "pldmgr.h"
#include <string.h>
#include <stdio.h>
#include <pthread.h>
#define MAX_HISTORY_ITEMS 20
#define MAX_PATH_LEN 512
static char history[MAX_HISTORY_ITEMS][MAX_PATH_LEN];
static int history_count = 0;
static pthread_mutex_t history_mutex = PTHREAD_MUTEX_INITIALIZER;
void history_mgr_add(const char *path) {
if (!path || path[0] == '\0') return;
if (!path || path[0] == '\0' || strlen(path) >= MAX_PATH_LEN) return;
pthread_mutex_lock(&history_mutex);
/* Check if already in history */
int existing_idx = -1;
for (int i = 0; i < history_count; i++) {
@@ -48,18 +51,24 @@ void history_mgr_add(const char *path) {
history_count++;
}
}
pthread_mutex_unlock(&history_mutex);
}
size_t history_mgr_to_json(char *buf, size_t max_size) {
if (!buf || !max_size) return 0;
char snapshot[MAX_HISTORY_ITEMS][MAX_PATH_LEN];
pthread_mutex_lock(&history_mutex);
int count = history_count;
memcpy(snapshot, history, sizeof(snapshot));
pthread_mutex_unlock(&history_mutex);
JsonListBuilder jb = { buf, max_size, 0, 1, 0 };
buf[0] = '\0';
json_append(&jb, "{\"history\":[\n");
for (int i = 0; i < history_count; i++) {
for (int i = 0; i < count; i++) {
char path_escaped[6 * 512];
json_escape_checked(&jb, history[i], path_escaped, sizeof(path_escaped));
json_escape_checked(&jb, snapshot[i], path_escaped, sizeof(path_escaped));
json_append(&jb, "%s \"%s\"", (i > 0) ? ",\n" : "", path_escaped);
}
+1 -1
View File
@@ -517,7 +517,7 @@ enum MHD_Result http_on_request(void *cls, struct MHD_Connection *conn,
int rc = request->kind == REQUEST_CONFIG
? config_handle_set_json(post->data) : sources_save(post->data, post->size);
if (rc != 0)
return finish_request_error(conn, request, MHD_HTTP_INTERNAL_SERVER_ERROR);
return finish_request_error(conn, request, rc == CONFIG_INVALID ? MHD_HTTP_BAD_REQUEST : MHD_HTTP_INTERNAL_SERVER_ERROR);
request->finished = 1;
struct MHD_Response *resp = create_buffer_response(
strlen(MSG_OK), (void *)MSG_OK, MHD_RESPMEM_PERSISTENT);
+91 -47
View File
@@ -6,6 +6,8 @@
#include <stdint.h>
#include <errno.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include "json_helpers.h"
#include "utils.h"
@@ -288,61 +290,103 @@ int json_extract_string_prefix(const char *start, const char *end, const char *k
return json_extract(start, end, key, out, capacity, 1);
}
int json_decode_string(const char *start, const char *end, char *out, size_t capacity) {
if (!out || !capacity || !start || !end || start > end) return -1;
out[0] = '\0';
JsonReader r = {start, end};
JsonStringSink sink = {.out = out, .capacity = capacity};
if (json_string(&r, &sink) || r.p != end || sink.truncated) {
out[0] = '\0'; return -1;
}
return 0;
}
static int json_visit(const char *start, const char *end, JsonMemberVisitor member,
JsonValueVisitor value, void *context, int object) {
if (!start || !end || start >= end) return -1;
JsonReader check = {start, end};
json_space(&check);
if (check.p == end || *check.p != (object ? '{' : '[') || json_value(&check, 0)) return -1;
json_space(&check);
if (check.p != end) return -1;
JsonReader r = {start, end};
json_space(&r); r.p++; json_space(&r);
char close = object ? '}' : ']';
if (*r.p == close) return 0;
for (;;) {
char key[256];
if (object) {
JsonStringSink sink = {.out = key, .capacity = sizeof(key)};
if (json_string(&r, &sink) || sink.truncated) return -1;
json_space(&r); r.p++; json_space(&r);
}
const char *first = r.p;
if (json_value(&r, 1)) return -1;
if (object ? member(key, first, r.p, context) : value(first, r.p, context)) return -1;
json_space(&r);
if (*r.p == close) return 0;
r.p++; json_space(&r);
}
}
int json_visit_object(const char *start, const char *end, JsonMemberVisitor visit, void *context) {
return visit ? json_visit(start, end, visit, NULL, context, 1) : -1;
}
int json_visit_array(const char *start, const char *end, JsonValueVisitor visit, void *context) {
return visit ? json_visit(start, end, NULL, visit, context, 0) : -1;
}
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 (!out_buf || !out_size) { errno = EINVAL; return -1; }
*out_buf = NULL; *out_size = 0;
FILE *f = fopen(path, "rb");
if (!f) return -1;
if (fseek(f, 0, SEEK_END)) { fclose(f); return -1; }
long length = ftell(f);
if (length < 0 || (uintmax_t)length >= SIZE_MAX || fseek(f, 0, SEEK_SET)) {
fclose(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);
errno = ENOMEM;
return -1;
}
nread = fread(buf, 1, (size_t)fsize, f);
fclose(f);
buf[nread] = '\0';
*out_buf = buf;
*out_size = nread;
char *buf = malloc((size_t)length + 1);
if (!buf) { fclose(f); errno = ENOMEM; return -1; }
size_t count = fread(buf, 1, (size_t)length, f);
int failed = count != (size_t)length || ferror(f);
if (fclose(f)) failed = 1;
if (failed) { free(buf); errno = EIO; return -1; }
buf[count] = '\0'; *out_buf = buf; *out_size = count;
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 (!path || (!data && size)) { errno = EINVAL; return -1; }
/* Sibling temporary file: concurrent writers never share a staging path,
* readers see the complete old or new inode, never a truncated file. */
size_t length = strlen(path);
if (length > SIZE_MAX - 16) { errno = ENAMETOOLONG; return -1; }
char *temp = malloc(length + 16);
if (!temp) return -1;
snprintf(temp, length + 16, "%s.tmp-XXXXXX", path);
int fd = mkstemp(temp);
if (fd < 0) { free(temp); return -1; }
FILE *f = fdopen(fd, "wb");
int failed = !f;
if (!f) close(fd);
else {
if ((size && fwrite(data, 1, size, f) != size) || fflush(f) || fsync(fileno(f))) failed = 1;
if (fclose(f)) failed = 1;
}
if (fwrite(data, 1, size, f) != size) {
fclose(f);
return -1;
if (!failed && rename(temp, path)) failed = 1;
int saved = errno;
if (failed) unlink(temp);
else {
/* Publication succeeded. Directory fsync is best-effort on filesystems
* that do not support it; a late sync failure cannot undo publication. */
char *slash = strrchr(temp, '/');
const char *parent = ".";
if (slash) { if (slash == temp) slash[1] = '\0'; else *slash = '\0'; parent = temp; }
int dir = open(parent, O_RDONLY | O_DIRECTORY);
if (dir >= 0) { fsync(dir); close(dir); }
}
fclose(f);
return 0;
free(temp); errno = saved;
return failed ? -1 : 0;
}
int mkdir_if_missing(const char *path) {
+3 -4
View File
@@ -813,10 +813,9 @@ static int slot_delete_unlocked(const char *slot_key) {
if (closedir(d) || rmdir(slot_dir)) return -1;
/* Remove from autoload.txt and favorites.txt */
pldmgr_autoload_update_config_entry(slot_key, NULL);
pldmgr_favorites_update_entry(slot_key, NULL);
return 0;
int autoload_result = pldmgr_autoload_update_config_entry(slot_key, NULL);
int favorites_result = pldmgr_favorites_update_entry(slot_key, NULL);
return autoload_result || favorites_result ? -1 : 0;
}
int slot_version_compare(const char *v1, const char *v2) {
+143 -194
View File
@@ -6,6 +6,8 @@
#include <unistd.h>
#include <sys/stat.h>
#include <dirent.h>
#include <errno.h>
#include <pthread.h>
#include "sources.h"
#include "repository.h"
@@ -28,62 +30,47 @@ typedef struct {
/* ── 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;
static pthread_mutex_t sources_mutex = PTHREAD_MUTEX_INITIALIZER;
typedef struct { SourceEntry *entries; int count; int found; } SourceParse;
static void default_source(SourceEntry *out) {
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;
}
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 < json + size &&
(p = memchr(p, '{', (size_t)(json + size - p))) != NULL) {
const char *end;
if (json_find_object_end(p, json + size, &end) != 0) 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));
if (strcmp(id, "default") == 0) { p = end; 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;
}
free(json);
*out_count = count;
snprintf(out[0].id, sizeof(out[0].id), "default");
snprintf(out[0].name, sizeof(out[0].name), "Official Repository");
snprintf(out[0].url, sizeof(out[0].url), "%s", REPOSITORY_SOURCE_URL);
}
static int source_item(const char *start, const char *end, void *context) {
SourceParse *parse = context;
SourceEntry entry = {.removable = 1};
if (json_extract_string(start, end, "id", entry.id, sizeof(entry.id)) ||
json_extract_string(start, end, "name", entry.name, sizeof(entry.name)) ||
json_extract_string(start, end, "url", entry.url, sizeof(entry.url)) ||
!entry.id[0] || !entry.name[0] || !entry.url[0]) return -1;
if (!strcmp(entry.id, "default")) return 0;
if (parse->count >= MAX_SOURCES) return -1;
for (int i = 0; i < parse->count; i++)
if (!strcmp(parse->entries[i].id, entry.id)) return -1;
parse->entries[parse->count++] = entry;
return 0;
}
static int source_member(const char *key, const char *start, const char *end, void *context) {
SourceParse *parse = context;
if (strcmp(key, "sources")) return 0;
if (parse->found++) return -1;
return json_visit_array(start, end, source_item, context);
}
static int parse_sources(const char *json, size_t size, SourceEntry *out, int *out_count) {
default_source(out);
SourceParse parse = {.entries=out, .count=1};
if (json_visit_object(json, json + size, source_member, &parse) || !parse.found) return -1;
*out_count = parse.count; return 0;
}
static int load_sources(SourceEntry *out, int *out_count) {
default_source(out); *out_count = 1;
char *json = NULL; size_t size = 0;
if (read_file_text(SOURCES_CONFIG_PATH, &json, &size)) return errno == ENOENT ? 0 : -1;
int result = parse_sources(json, size, out, out_count);
free(json); return result;
}
static int save_sources(SourceEntry *sources, int count) {
size_t size = MAX_SOURCES * (6 * sizeof(SourceEntry) + 128) + 64;
@@ -121,7 +108,7 @@ int sources_list_json(char *buf, size_t size) {
SourceEntry *sources = calloc(MAX_SOURCES, sizeof(SourceEntry));
if (!sources) return -1;
int count = 0;
load_sources(sources, &count);
if (load_sources(sources, &count)) { free(sources); return -1; }
JsonListBuilder jb = { buf, size, 0, 1, 0 };
buf[0] = '\0';
@@ -145,138 +132,25 @@ int sources_list_json(char *buf, size_t size) {
}
int sources_save(const char *json, size_t len) {
if (!json || !len) return CONFIG_INVALID;
SourceEntry *sources = calloc(MAX_SOURCES, sizeof(SourceEntry));
if (!sources) return -1;
int count = 0;
if (!json || len == 0) {
free(sources);
return -1;
}
/* 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) { free(sources); return -1; }
arr_start = strchr(arr_start, '[');
if (!arr_start) { free(sources); return -1; }
const char *p = arr_start + 1;
while (count < MAX_SOURCES && p < json + len &&
(p = memchr(p, '{', (size_t)(json + len - p))) != NULL) {
const char *end;
if (json_find_object_end(p, json + len, &end) != 0) 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; continue; }
if (!name[0] || !url[0] || !id[0]) { p = end; 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;
}
int ret = save_sources(sources, count);
free(sources);
return ret;
if (parse_sources(json, len, sources, &count)) { free(sources); return CONFIG_INVALID; }
pthread_mutex_lock(&sources_mutex);
int result = save_sources(sources, count);
pthread_mutex_unlock(&sources_mutex);
free(sources); return result;
}
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_limited(url, tmp_path, PLDMGR_MANIFEST_DOWNLOAD_LIMIT) != 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 before payloads key */
{
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 (!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);
static int merge_source(const char *url, const char *source_name, char *msg_buf, size_t msg_size) {
SourceEntry *sources = calloc(MAX_SOURCES, sizeof(SourceEntry));
if (!sources) {
snprintf(msg_buf, msg_size, "Memory allocation failed");
return -1;
}
int src_count = 0;
load_sources(sources, &src_count);
if (load_sources(sources, &src_count)) { free(sources); snprintf(msg_buf, msg_size, "Failed to read sources"); return -1; }
if (src_count >= MAX_SOURCES) {
free(sources);
@@ -293,7 +167,14 @@ int sources_add(const char *url, char *msg_buf, size_t msg_size) {
}
char new_id[64];
snprintf(new_id, sizeof(new_id), "source_%d", (int)time(NULL));
char hash[16]; slot_source_hash(url, hash, sizeof(hash));
snprintf(new_id, sizeof(new_id), "source_%s", hash);
for (int suffix = 1;; suffix++) {
int found = 0;
for (int i = 0; i < src_count; i++) if (!strcmp(sources[i].id, new_id)) found = 1;
if (!found) break;
snprintf(new_id, sizeof(new_id), "source_%s_%d", hash, suffix);
}
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);
@@ -312,7 +193,69 @@ int sources_add(const char *url, char *msg_buf, size_t msg_size) {
return 0;
}
int sources_remove(int index, char *msg_buf, size_t msg_size) {
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-XXXXXX", BASE_DATA_DIR);
int fd = mkstemp(tmp_path);
if (fd < 0) { snprintf(msg_buf, msg_size, "Failed to create validation file"); return -1; }
if (close(fd)) { remove(tmp_path); snprintf(msg_buf, msg_size, "Failed to close validation file"); return -1; }
if (strlen(url) >= sizeof(((SourceEntry *)0)->url)) {
remove(tmp_path); snprintf(msg_buf, msg_size, "URL is too long"); return -1;
}
if (download_to_file_limited(url, tmp_path, PLDMGR_MANIFEST_DOWNLOAD_LIMIT) != 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);
/* Decode the actual top-level name, including escaped Unicode/quotes. */
json_extract_string_prefix(json, json + json_size, "name", source_name, sizeof(source_name));
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);
pthread_mutex_lock(&sources_mutex);
int result = merge_source(url, source_name, msg_buf, msg_size);
pthread_mutex_unlock(&sources_mutex);
return result;
}
static int remove_source(int index, char *msg_buf, size_t msg_size) {
if (index <= 0) {
snprintf(msg_buf, msg_size, "Cannot remove the default source");
return -1;
@@ -324,7 +267,7 @@ int sources_remove(int index, char *msg_buf, size_t msg_size) {
return -1;
}
int count = 0;
load_sources(sources, &count);
if (load_sources(sources, &count)) { free(sources); snprintf(msg_buf, msg_size, "Failed to read sources"); return -1; }
if (index >= count) {
free(sources);
@@ -348,6 +291,13 @@ int sources_remove(int index, char *msg_buf, size_t msg_size) {
return 0;
}
int sources_remove(int index, char *msg_buf, size_t msg_size) {
pthread_mutex_lock(&sources_mutex);
int result = remove_source(index, msg_buf, msg_size);
pthread_mutex_unlock(&sources_mutex);
return result;
}
/* ── Hash-based cache paths ────────────────────────────────── */
static void get_cache_paths(const char *url, char *cache_path, size_t csize,
@@ -383,9 +333,12 @@ int sources_ensure_fresh(const char *url, int force_refresh) {
}
char tmp_path[512];
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp", cache_path);
snprintf(tmp_path, sizeof(tmp_path), "%s.tmp-XXXXXX", cache_path);
int fd = mkstemp(tmp_path);
if (fd < 0) return -1;
if (close(fd)) { remove(tmp_path); return -1; }
if (download_to_file_limited(url, tmp_path, PLDMGR_MANIFEST_DOWNLOAD_LIMIT) != 0) {
return -1;
remove(tmp_path); return -1;
}
char *json = NULL;
@@ -412,15 +365,11 @@ int sources_ensure_fresh(const char *url, int force_refresh) {
return -1;
}
tf = fopen(ts_path, "w");
if (tf) {
fprintf(tf, "%ld", (long)now);
fclose(tf);
}
if (strcmp(url, REPOSITORY_SOURCE_URL) == 0) {
config_write_last_update((long)now);
}
char timestamp[64];
int timestamp_size = snprintf(timestamp, sizeof(timestamp), "%ld", (long)now);
if (timestamp_size < 0 || (size_t)timestamp_size >= sizeof(timestamp) ||
write_file_text(ts_path, timestamp, (size_t)timestamp_size)) return -1;
if (!strcmp(url, REPOSITORY_SOURCE_URL) && config_write_last_update((long)now)) return -1;
return 0;
}
@@ -433,7 +382,7 @@ size_t sources_multi_repository_list_json(char *buf, size_t size, int force_refr
return 0;
}
int src_count = 0;
load_sources(sources, &src_count);
if (load_sources(sources, &src_count)) { free(sources); buf[0] = 0; return 0; }
/* Single-source mode: if multi_sources is off, only use source 0 */
PldmgrConfig cfg;
@@ -583,7 +532,7 @@ int sources_repo_install(const char *source_url, const char *id,
return -1;
}
int src_count = 0;
load_sources(sources, &src_count);
if (load_sources(sources, &src_count)) { free(sources); json_append(&jb, "{\"ok\":false,\"message\":\"Failed to read sources\"}"); return -1; }
/* Match source */
SourceEntry src;
@@ -839,9 +788,9 @@ int sources_repo_install(const char *source_url, const char *id,
int old_cleanup_failed = 0;
/* Retire a conflicting source only after the new slot is usable. */
if (is_mode_replace && replace_slot && replace_slot[0] && strcmp(replace_slot, target_slot)) {
pldmgr_autoload_update_config_entry(replace_slot, target_slot);
pldmgr_favorites_update_entry(replace_slot, target_slot);
if (slot_delete(replace_slot)) {
int autoload_result = pldmgr_autoload_update_config_entry(replace_slot, target_slot);
int favorites_result = pldmgr_favorites_update_entry(replace_slot, target_slot);
if (autoload_result || favorites_result || slot_delete(replace_slot)) {
old_cleanup_failed = 1;
pldmgr_log("[PLDMGR] New repository slot committed; old slot cleanup incomplete: %s\n", replace_slot);
}
+185
View File
@@ -0,0 +1,185 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <dirent.h>
#include <pthread.h>
#include <stdatomic.h>
#include "config.h"
#include "json_helpers.h"
#include "history_mgr.h"
#include "sources.h"
#include "pldmgr.h"
#include "slot.h"
#include "sha256.h"
#define CHECK(x) do { if (!(x)) { fprintf(stderr,"FAIL %d: %s\n",__LINE__,#x); exit(1); } } while (0)
static const char *fault_name;
static int fault_at, fault_hits;
static FILE *bad_read;
static _Atomic int aborts;
static int hit(const char *name) { if (fault_name && !strcmp(name,fault_name) && ++fault_hits == fault_at) { errno=ENOSPC; return 1; } return 0; }
size_t config_test_fwrite(const void *p,size_t a,size_t b,FILE *f) { if(hit("write")) return 0; return fwrite(p,a,b,f); }
size_t config_test_fread(void *p,size_t a,size_t b,FILE *f) { if(hit("read")) { bad_read=f; return 0; } return fread(p,a,b,f); }
int config_test_ferror(FILE *f) { return f==bad_read || ferror(f); }
int config_test_fflush(FILE *f) { int rc=fflush(f); return hit("flush") ? -1 : rc; }
int config_test_fclose(FILE *f) { if(f==bad_read)bad_read=NULL; int rc=fclose(f); return hit("close") ? -1 : rc; }
int config_test_fsync(int fd) { return hit("sync") ? -1 : fsync(fd); }
int config_test_rename(const char *a,const char *b) { return hit("rename") ? -1 : rename(a,b); }
int config_test_mkstemp(char *p) { return hit("create") ? -1 : mkstemp(p); }
void pldmgr_log(const char *fmt,...) { (void)fmt; }
void pldmgr_autoload_abort(void) { aborts++; }
int pldmgr_autoload_update_config_entry(const char *a,const char *b) { return config_update_list_entry(AUTOLOAD_CONFIG_PATH,a,b); }
static _Atomic int downloads;
int download_to_file_limited(const char *url,const char *path,size_t limit) {
(void)limit; CHECK(strstr(url,"fixture://")); downloads++;
const char *json="{\"name\":\"F\\u00edxture\",\"payloads\":[{\"id\":\"safe\",\"name\":\"Safe\",\"filename\":\"safe.elf\",\"url\":\"http://example.invalid/safe.elf\"}]}";
FILE *f=fopen(path,"wb"); CHECK(f); CHECK(fwrite(json,1,strlen(json),f)==strlen(json)); CHECK(!fclose(f));
usleep(1000); return 0;
}
int download_to_file(const char *url,const char *path) { return download_to_file_limited(url,path,0); }
static void fault(const char *name,int at) { fault_name=name; fault_at=at; fault_hits=0; }
static char *read_text(const char *path) { char *text; size_t size; CHECK(!read_file_text(path,&text,&size)); return text; }
static void expect_text(const char *path,const char *expected) { char *text=read_text(path); CHECK(!strcmp(text,expected)); free(text); }
static void scalar(void) {
CHECK(config_read_bool("AUTOLOAD_ENABLED",1)==1);
CHECK(!config_handle_set_json("{\"AUTO_BROWSER_OPEN\":false}"));
PldmgrConfig cfg; config_read(&cfg); CHECK(cfg.auto_browser_open==0); CHECK(cfg.autoload_delay==5); CHECK(cfg.auto_install_app==1);
CHECK(!config_handle_set_json("{\"SCAN_USB_PAYLOADS\":true}")); config_read(&cfg); CHECK(cfg.scan_usb_payloads==1 && cfg.auto_browser_open==0);
CHECK(!config_handle_set_json("{\"AUTO_INSTALL_APP\":false}")); config_read(&cfg); CHECK(cfg.auto_install_app==0 && cfg.scan_usb_payloads==1);
CHECK(!config_handle_set_json("{\"MULTI_SOURCES_ENABLED\":true}")); config_read(&cfg); CHECK(cfg.multi_sources_enabled==1);
CHECK(!config_handle_set_json("{\"AUTOLOAD_DELAY\":0}")); config_read(&cfg); CHECK(cfg.autoload_delay==0);
CHECK(!config_write_last_update(123456));
CHECK(!config_handle_set_json("{\"AUTOLOAD_ENABLED\":true}")); config_read(&cfg); CHECK(cfg.autoload_enabled==1 && cfg.last_repository_update==123456 && cfg.autoload_delay==0);
CHECK(!config_handle_set_json("{\"AUTOLOAD_ENABLED\":false}")); CHECK(aborts==1);
CHECK(!config_handle_set_json("{\"nested\":{\"AUTOLOAD_DELAY\":20},\"FAVORITES_INITIALIZED\":true}")); config_read(&cfg); CHECK(cfg.autoload_delay==0);
CHECK(!config_handle_set_json("{\"AUTOLOAD_LIST\":\"local/żółw,!1000,local/next\",\"FAVORITES\":[\"local/żółw\",\"local/a\\\"b\"]}"));
expect_text(AUTOLOAD_CONFIG_PATH,"local/żółw\n!1000\nlocal/next\n");
expect_text(FAVORITES_CONFIG_PATH,"local/żółw\nlocal/a\"b\n");
CHECK(!config_handle_set_json("{\"FAVORITES\":\"\",\"AUTOLOAD_LIST\":\"\"}"));
expect_text(AUTOLOAD_CONFIG_PATH,""); expect_text(FAVORITES_CONFIG_PATH,"");
CHECK(!config_handle_set_json("{\"AUTOLOAD_DELAY\":3600}"));
CHECK(!config_upsert_value("CUSTOM", "keep")); CHECK(!config_write_last_update(999)); CHECK(!config_handle_set_json("{\"AUTOLOAD_DELAY\":5}"));
char *saved=read_text(PLDMGR_CONFIG_PATH); CHECK(strstr(saved,"CUSTOM=keep\n")); free(saved);
puts("Config: individual settings, false/zero, timestamps, Unicode, empty and array lists passed");
}
static void validation(void) {
CHECK(!config_handle_set_json("{\"AUTOLOAD_ENABLED\":true,\"FAVORITES\":\"local/keep\",\"AUTOLOAD_LIST\":\"local/keep\"}"));
char *old=read_text(PLDMGR_CONFIG_PATH);
const char *invalid[]={"{", "{} garbage", "[]", "{\"AUTOLOAD_ENABLED\":false,\"AUTOLOAD_DELAY\":-1}",
"{\"FAVORITES\":\"local/change\",\"AUTO_BROWSER_OPEN\":0}", "{\"AUTOLOAD_ENABLED\":null}", "{\"AUTOLOAD_ENABLED\":\"true\"}",
"{\"AUTOLOAD_DELAY\":1.5}", "{\"AUTOLOAD_DELAY\":1e2}", "{\"AUTOLOAD_DELAY\":3601}", "{\"AUTOLOAD_DELAY\":999999999999999999999999}",
"{\"AUTOLOAD_ENABLED\":true,\"AUTOLOAD_ENABLED\":false}", "{\"AUTOLOAD_DELAY\":1,\"AUTOLOAD_D\\u0045LAY\":2}",
"{\"FAVORITES\":[1]}", "{\"FAVORITES\":[\"local/a,b\"]}", "{\"FAVORITES\":null}", "{\"AUTOLOAD_LIST\":\"a\\nb\"}",
"{\"FAVORITES\":\"a\\u0000b\"}", "{\"FAVORITES\":\"a\\u0001b\"}", "{\"FAVORITES\":\"a\",\"FAVORITES\":\"b\"}"};
for(size_t i=0;i<sizeof(invalid)/sizeof(*invalid);i++) {
CHECK(config_handle_set_json(invalid[i])==CONFIG_INVALID);
expect_text(PLDMGR_CONFIG_PATH,old); expect_text(FAVORITES_CONFIG_PATH,"local/keep\n"); expect_text(AUTOLOAD_CONFIG_PATH,"local/keep\n"); CHECK(aborts==0);
}
char huge[4200]; memset(huge,'a',sizeof(huge)); memcpy(huge,"{\"FAVORITES\":\"",14); memcpy(huge+sizeof(huge)-3,"\"}",3);
CHECK(config_handle_set_json(huge)==CONFIG_INVALID); free(old);
CHECK(!sources_save("{\"sources\":[]}",14)); char *sources=read_text(SOURCES_CONFIG_PATH);
const char *bad[]={"{\"sources\":[]", "{\"sources\":[],\"sources\":[]}", "{\"sources\":[{}]}", "{\"nested\":{\"sources\":[]}}", "{\"sources\":null}", "{\"sources\":[{\"id\":\"x\",\"name\":\"X\",\"url\":false}]}"};
for(size_t i=0;i<sizeof(bad)/sizeof(*bad);i++) { CHECK(sources_save(bad[i],strlen(bad[i]))==CONFIG_INVALID); expect_text(SOURCES_CONFIG_PATH,sources); }
free(sources); puts("Config/source validation: malformed input rejected without writes or aborts");
}
static void atomic_files(void) {
char path[512]; snprintf(path,sizeof(path),"%s/atomic.txt",BASE_DATA_DIR);
CHECK(!write_file_text(path,"old",3));
const char *operations[]={"create","write","flush","sync","close","rename"};
for(size_t i=0;i<sizeof(operations)/sizeof(*operations);i++) { fault(operations[i],1); CHECK(write_file_text(path,"new",3)==-1); fault(NULL,0); expect_text(path,"old"); }
fault("read",1); char *text=(char *)1; size_t size=1; CHECK(read_file_text(path,&text,&size)==-1 && text==NULL && size==0); fault(NULL,0);
fault("close",1); CHECK(read_file_text(path,&text,&size)==-1 && text==NULL && size==0); fault(NULL,0);
CHECK(!config_handle_set_json("{\"AUTOLOAD_ENABLED\":false,\"FAVORITES\":\"old\",\"AUTOLOAD_LIST\":\"old\"}"));
fault("rename",2); CHECK(config_handle_set_json("{\"AUTOLOAD_ENABLED\":true,\"FAVORITES\":\"new\",\"AUTOLOAD_LIST\":\"new\"}")==-1); fault(NULL,0);
expect_text(AUTOLOAD_CONFIG_PATH,"new\n"); expect_text(FAVORITES_CONFIG_PATH,"old\n"); PldmgrConfig cfg; config_read(&cfg); CHECK(!cfg.autoload_enabled);
CHECK(!config_handle_set_json("{\"FAVORITES\":\"a,b,c\"}")); fault("rename",1); CHECK(pldmgr_favorites_update_entry("b","longer")==-1); fault(NULL,0); expect_text(FAVORITES_CONFIG_PATH,"a\nb\nc\n");
CHECK(!pldmgr_favorites_update_entry("b","longer")); expect_text(FAVORITES_CONFIG_PATH,"a\nlonger\nc\n");
CHECK(!pldmgr_favorites_update_entry("a",NULL)); expect_text(FAVORITES_CONFIG_PATH,"longer\nc\n");
CHECK(!sources_save("{\"sources\":[]}",14));
char *source_old=read_text(SOURCES_CONFIG_PATH);
const char *source_new="{\"sources\":[{\"id\":\"test\",\"name\":\"Test\",\"url\":\"fixture://test\"}]}";
fault("rename",1); CHECK(sources_save(source_new,strlen(source_new))==-1); fault(NULL,0); expect_text(SOURCES_CONFIG_PATH,source_old);
CHECK(!sources_save(source_new,strlen(source_new))); free(source_old); source_old=read_text(SOURCES_CONFIG_PATH);
char message[256]; fault("rename",1); CHECK(sources_remove(1,message,sizeof(message))==-1); fault(NULL,0); expect_text(SOURCES_CONFIG_PATH,source_old);
free(source_old);
char *config_old=read_text(PLDMGR_CONFIG_PATH);
fault("rename",1); CHECK(config_handle_set_json("{\"SCAN_USB_PAYLOADS\":true}")==-1); fault(NULL,0); expect_text(PLDMGR_CONFIG_PATH,config_old); free(config_old);
DIR *dir=opendir(BASE_DATA_DIR); CHECK(dir); struct dirent *entry; while((entry=readdir(dir))) CHECK(!strstr(entry->d_name,".tmp-")); closedir(dir);
puts("Atomic persistence: create/write/flush/sync/close/rename/read failures preserve old files; multi-file policy and remaps passed");
}
static pthread_mutex_t gate_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t gate_condition = PTHREAD_COND_INITIALIZER;
static int gate_count;
static void start_together(void) {
pthread_mutex_lock(&gate_mutex);
if (++gate_count == 8) pthread_cond_broadcast(&gate_condition);
else while (gate_count < 8) pthread_cond_wait(&gate_condition,&gate_mutex);
pthread_mutex_unlock(&gate_mutex);
}
static void *settings_worker(void *argument) {
long id=(long)argument; start_together();
for(int n=0;n<200;n++) {
switch(id) {
case 0: CHECK(!config_handle_set_json("{\"AUTO_BROWSER_OPEN\":false}")); break;
case 1: CHECK(!config_handle_set_json("{\"SCAN_USB_PAYLOADS\":true}")); break;
case 2: CHECK(!config_handle_set_json("{\"AUTO_INSTALL_APP\":false}")); break;
case 3: CHECK(!config_write_last_update(777777)); break;
case 4: CHECK(!config_handle_set_json("{\"FAVORITES\":\"local/a,local/b,local/c\"}")); break;
case 5: CHECK(!config_handle_set_json("{\"AUTOLOAD_LIST\":\"local/d,!1000,local/e\"}")); break;
case 6: CHECK(!config_update_list_entry(FAVORITES_CONFIG_PATH,"missing","unused")); break;
case 7: { PldmgrConfig cfg; config_read(&cfg); CHECK(cfg.autoload_delay==5); char out[4096]; int exists; CHECK(!config_get_favorites_csv(out,sizeof(out),&exists)); break; }
}
} return NULL;
}
static void parallel_settings(void) {
pthread_t threads[8];
for(long i=0;i<8;i++) CHECK(!pthread_create(&threads[i],NULL,settings_worker,(void *)i));
for(int i=0;i<8;i++) CHECK(!pthread_join(threads[i],NULL));
PldmgrConfig cfg; config_read(&cfg); CHECK(!cfg.auto_browser_open && cfg.scan_usb_payloads && !cfg.auto_install_app && cfg.last_repository_update==777777);
expect_text(FAVORITES_CONFIG_PATH,"local/a\nlocal/b\nlocal/c\n"); expect_text(AUTOLOAD_CONFIG_PATH,"local/d\n!1000\nlocal/e\n");
puts("Config concurrency: 1,600 updates/remaps/reads preserve independent fields, timestamps and lists");
}
typedef struct { int count; char entries[20][512]; } HistoryCheck;
static int history_item(const char *a,const char *b,void *context) {
HistoryCheck *check=context; if(check->count==20) return -1;
char *path=check->entries[check->count]; if(json_decode_string(a,b,path,512))return -1;
for(int i=0;i<check->count;i++)if(!strcmp(check->entries[i],path))return -1;
check->count++; return 0;
}
static int history_member(const char *key,const char *a,const char *b,void *context) { return strcmp(key,"history") ? -1 : json_visit_array(a,b,history_item,context); }
static void *history_worker(void *argument) {
long id=(long)argument;
for(int i=0;i<2000;i++) {
if(id<4) { char path[512]; snprintf(path,sizeof(path),"local/thread-%ld-item-%d",id,i%25); history_mgr_add(path); }
else { char json[65536]; size_t n=history_mgr_to_json(json,sizeof(json)); CHECK(n); HistoryCheck check={0}; CHECK(!json_visit_object(json,json+n,history_member,&check)); }
} return NULL;
}
static void parallel_history(void) {
pthread_t threads[8]; for(long i=0;i<8;i++)CHECK(!pthread_create(&threads[i],NULL,history_worker,(void *)i));
for(int i=0;i<8;i++)CHECK(!pthread_join(threads[i],NULL));
for(int i=0;i<25;i++) { char path[64]; snprintf(path,sizeof(path),"local/item-%d",i); history_mgr_add(path); }
history_mgr_add("local/item-10"); char json[65536]; size_t size=history_mgr_to_json(json,sizeof(json)); CHECK(size); puts(json);
HistoryCheck check={0}; CHECK(!json_visit_object(json,json+size,history_member,&check)); CHECK(check.count==20 && !strcmp(check.entries[0],"local/item-10") && !strcmp(check.entries[1],"local/item-24"));
char long_path[600]; memset(long_path,'x',599); long_path[599]=0; history_mgr_add(long_path); history_mgr_add(long_path);
size=history_mgr_to_json(json,sizeof(json)); check=(HistoryCheck){0}; CHECK(!json_visit_object(json,json+size,history_member,&check)); CHECK(check.count==20);
puts("History concurrency: 8,000 launches and 8,000 snapshots preserve bounds/uniqueness; deterministic ordering passed");
}
static void *source_worker(void *argument) { char url[128],msg[256]; snprintf(url,sizeof(url),"fixture://source-%ld",(long)argument); CHECK(!sources_add(url,msg,sizeof(msg))); return NULL; }
static void parallel_sources(void) {
CHECK(!sources_save("{\"sources\":[]}",14)); pthread_t threads[12];
for(long i=0;i<12;i++)CHECK(!pthread_create(&threads[i],NULL,source_worker,(void *)i));
for(int i=0;i<12;i++)CHECK(!pthread_join(threads[i],NULL));
char json[20000]; CHECK(!sources_list_json(json,sizeof(json))); puts(json); CHECK(downloads==12);
for(int i=0;i<12;i++) { char url[128]; snprintf(url,sizeof(url),"fixture://source-%d",i); CHECK(strstr(json,url)); }
for(int i=0;i<12;i++) { char msg[128]; CHECK(!sources_remove(1,msg,sizeof(msg))); }
CHECK(!sources_list_json(json,sizeof(json))); CHECK(!strstr(json,"fixture://")); puts("Source concurrency: 12 simultaneous validations/adds retained; removals passed");
}
int main(int argc,char **argv) {
CHECK(argc==2); CHECK(!ensure_dir_recursive(BASE_DATA_DIR));
if(!strcmp(argv[1],"scalar"))scalar(); else if(!strcmp(argv[1],"validation"))validation();
else if(!strcmp(argv[1],"atomic"))atomic_files(); else if(!strcmp(argv[1],"settings"))parallel_settings();
else if(!strcmp(argv[1],"history"))parallel_history(); else if(!strcmp(argv[1],"sources"))parallel_sources(); else return 2;
return 0;
}
+18
View File
@@ -0,0 +1,18 @@
#include <stdio.h>
#include <unistd.h>
size_t config_test_fwrite(const void *, size_t, size_t, FILE *);
size_t config_test_fread(void *, size_t, size_t, FILE *);
int config_test_ferror(FILE *);
int config_test_fflush(FILE *);
int config_test_fclose(FILE *);
int config_test_fsync(int);
int config_test_rename(const char *, const char *);
int config_test_mkstemp(char *);
#define fwrite config_test_fwrite
#define fread config_test_fread
#define ferror config_test_ferror
#define fflush config_test_fflush
#define fclose config_test_fclose
#define fsync config_test_fsync
#define rename config_test_rename
#define mkstemp config_test_mkstemp
+4 -2
View File
@@ -27,18 +27,20 @@ size_t test_post_size;
void pldmgr_log(const char *fmt, ...) { (void)fmt; }
void log_server_set_active(void) { test_active_calls++; }
#ifndef PLDMGR_TEST_REAL_CONFIG
void config_read(PldmgrConfig *cfg) { memset(cfg, 0, sizeof(*cfg)); cfg->autoload_delay = 5; }
int config_read_bool(const char *key, int fallback) { (void)key; return fallback; }
int config_handle_set_json(const char *data) {
test_config_saves++; test_mutation_calls++;
test_post_size = strlen(data);
return strcmp(data, "fail_test") == 0 ? -1 : 0;
return strcmp(data, "fail_test") == 0 ? -1 : (strcmp(data, "invalid_test") == 0 ? CONFIG_INVALID : 0);
}
int config_get_favorites_csv(char *out, size_t size, int *exists) {
if (size) out[0] = '\0';
*exists = 0;
return test_json_failure ? -1 : 0;
}
#endif
void pldmgr_autoload_get_status(int *total, int *done, char *current) {
*total = 0; *done = 0; current[0] = '\0';
}
@@ -92,7 +94,7 @@ int sources_list_json(char *out, size_t size) { json(out, size, "{\"sources\":[]
int sources_save(const char *data, size_t size) {
test_source_saves++; test_mutation_calls++;
test_post_size = size;
return strcmp(data, "fail_test") == 0 ? -1 : 0;
return strcmp(data, "fail_test") == 0 ? -1 : (strcmp(data, "invalid_test") == 0 ? CONFIG_INVALID : 0);
}
int sources_add(const char *url, char *msg, size_t size) {
test_mutation_calls++; (void)url; snprintf(msg, size, "Test source"); return 0;
+1 -1
View File
@@ -37,7 +37,7 @@ int integrity_openat(int parent,const char *part,int flags,...) {
}
static int sockets, history;
void pldmgr_log(const char *fmt,...) {(void)fmt;}
void pldmgr_autoload_update_config_entry(const char *a,const char *b){(void)a;(void)b;}
int pldmgr_autoload_update_config_entry(const char *a,const char *b){(void)a;(void)b;return 0;}
void history_mgr_add(const char *path){(void)path;history++;}
int integrity_socket(int domain,int type,int protocol){(void)domain;(void)type;(void)protocol;sockets++;return open("/dev/null",O_WRONLY);}
int integrity_connect(int fd,const struct sockaddr *addr,socklen_t length){(void)fd;(void)addr;(void)length;return 0;}
+1 -1
View File
@@ -11,7 +11,7 @@
static const char *manifest;
static int first_calls,retry_calls,fail_first;
void pldmgr_log(const char *fmt,...){(void)fmt;}
void pldmgr_autoload_update_config_entry(const char *a,const char *b){(void)a;(void)b;}
int pldmgr_autoload_update_config_entry(const char *a,const char *b){(void)a;(void)b;return 0;}
static int write_bytes(const char *path,const char *text){FILE*f=fopen(path,"wb");if(!f)return -1;int rc=fwrite(text,1,strlen(text),f)==strlen(text)?0:-1;if(fclose(f))rc=-1;return rc;}
int download_to_file(const char *url,const char *path){
if(!strcmp(url,REPOSITORY_SOURCE_URL)){char *text=NULL;size_t size=0;if(read_file_text(manifest,&text,&size))return -1;int rc=write_bytes(path,text);free(text);return rc;}
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""Actual config/source persistence and history under faults and concurrent access."""
import argparse
import json
import os
from pathlib import Path
import shlex
import subprocess
import sys
import tempfile
ROOT = Path(__file__).resolve().parents[1]
parser = argparse.ArgumentParser()
parser.add_argument('--thread-sanitize', action='store_true')
parser.add_argument('--legacy', action='store_true')
parser.add_argument('--legacy-history', action='store_true')
args = parser.parse_args()
if args.legacy_history and not args.thread_sanitize: parser.error("--legacy-history requires --thread-sanitize")
os.environ.setdefault('ASAN_OPTIONS', ('detect_leaks=0:halt_on_error=1' if sys.platform == 'darwin' else 'detect_leaks=1:halt_on_error=1'))
os.environ.setdefault('UBSAN_OPTIONS', 'halt_on_error=1:print_stacktrace=1')
os.environ.setdefault('TSAN_OPTIONS', 'halt_on_error=1')
with tempfile.TemporaryDirectory(prefix='pldmgr-config-') as temp:
build = Path(temp)
storage = build / 'data'
storage.mkdir()
(build / 'pldmgr.h').write_text((ROOT / 'include/pldmgr.h').read_text().replace('/data/pldmgr', str(storage)))
(build / 'assets_cacert_pem.h').write_text('static const unsigned char assets_cacert_pem[]={0};\nstatic const unsigned int assets_cacert_pem_len=0;\n')
flags = shlex.split(os.environ.get('HOST_CC', 'clang')) + ['-std=c11', '-D_DEFAULT_SOURCE', '-g', '-O1', '-Wall', '-Wextra', '-pthread', '-ffunction-sections', '-fdata-sections', '-I'+str(build), '-Iinclude']
flags += ['-fsanitize=thread' if args.thread_sanitize else '-fsanitize=address,undefined', '-fno-omit-frame-pointer']
objects = []
for name in ['config', 'sources', 'history_mgr', 'json_helpers', 'utils', 'sha256', 'slot', 'repository']:
obj = build / (name+'.o')
source = f'src/{name}.c'
extra = ['-include', 'tests/config_hooks.h'] if name == 'json_helpers' else []
if args.legacy and name == 'config':
# Adapt only the old void remap symbol, unused by the scalar reproduction.
snapshot = build / 'config_legacy.c'
old = subprocess.check_output(['git', 'show', 'cc0fc90:src/config.c'], cwd=ROOT, text=True)
old = old.replace('void pldmgr_favorites_update_entry', 'void legacy_favorites_update_entry')
old += '\nint config_update_list_entry(const char *p,const char *a,const char *b) {(void)p;(void)a;(void)b;return 0;}\nint pldmgr_favorites_update_entry(const char *a,const char *b) {(void)a;(void)b;return 0;}\n'
snapshot.write_text(old)
source = str(snapshot)
if args.legacy_history and name == 'history_mgr':
snapshot = build / 'history_legacy.c'
snapshot.write_text(subprocess.check_output(['git', 'show', 'cc0fc90:src/history_mgr.c'], cwd=ROOT, text=True))
source = str(snapshot)
if name == 'repository': extra += ['-Ddownload_to_file=config_unused_download', '-Ddownload_to_file_limited=config_unused_download_limited']
subprocess.run(flags+extra+['-c', source, '-o', str(obj)], cwd=ROOT, check=True)
objects.append(str(obj))
binary = build / 'config_tests'
gc = '-Wl,-dead_strip' if sys.platform == 'darwin' else '-Wl,--gc-sections'
subprocess.run(flags+objects+['tests/config_driver.c', gc, '-o', str(binary)], cwd=ROOT, check=True)
cases = ['history'] if args.legacy_history else (['scalar'] if args.legacy else (['settings', 'history', 'sources'] if args.thread_sanitize else ['scalar','validation','atomic','settings','history','sources']))
for case in cases:
# Independent clean storage for each scenario.
for path in storage.iterdir():
if path.is_file(): path.unlink()
result = subprocess.run([str(binary), case], cwd=ROOT, text=True, capture_output=True)
if args.legacy_history:
assert result.returncode and 'ThreadSanitizer: data race' in result.stderr, result.stderr
print('Previous history implementation reproduces a ThreadSanitizer data race')
continue
if args.legacy:
assert result.returncode and 'cfg.auto_browser_open==0' in result.stderr, result.stderr
print('Previous implementation reproduces ignored individual setting: '+result.stderr.strip().splitlines()[0])
continue
if result.returncode:
print(result.stdout); print(result.stderr, file=sys.stderr)
raise SystemExit(result.returncode)
start = result.stdout.find('{')
if start >= 0:
document, _ = json.JSONDecoder().raw_decode(result.stdout[start:])
if case == 'history':
expected = ['local/item-10']+[f'local/item-{i}' for i in range(24,4,-1) if i != 10]
assert document['history'] == expected, document
if case == 'sources':
sources = document['sources']
assert len(sources)==13 and len({s['id'] for s in sources})==13
assert all(s['name']=='Fíxture' for s in sources[1:])
assert {s['url'] for s in sources[1:]} == {f'fixture://source-{i}' for i in range(12)}
print('\n'.join(line for line in result.stdout.splitlines() if line.startswith(('Config','Atomic','History','Source'))))
print('Config/history/source checks passed'+(' under ThreadSanitizer' if args.thread_sanitize else ''))
+44 -1
View File
@@ -359,6 +359,43 @@ def network_tests(binary, storage, security_only=False, legacy_security=False):
server.communicate()
def config_network_tests(binary, storage):
server = subprocess.Popen([str(binary), "--serve"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)
try:
line = server.stdout.readline().strip()
assert line.startswith("PORT "), line
port = int(line.split()[1])
def request(method, route, body=None):
connection = http.client.HTTPConnection("127.0.0.1", port, timeout=10)
try:
connection.request(method, route, body.encode("utf-8") if isinstance(body, str) else body, {"X-PLDMGR-Request":"1"})
response = connection.getresponse()
return response.status, response.read()
finally: connection.close()
assert request("POST", "/set_config", '{"AUTO_BROWSER_OPEN":false}')[0] == 200
status, body = request("GET", "/get_config")
assert status == 200 and json.loads(body)["AUTO_BROWSER_OPEN"] is False
for bad in ['{', '{"AUTOLOAD_ENABLED":false,"AUTOLOAD_DELAY":-1}', '{"FAVORITES":[1]}']:
assert request("POST", "/set_config", bad)[0] == 400
# A genuine filesystem failure must produce 500 and retain old config.
old = (storage / "pldmgr_config.txt").read_bytes()
(storage / "favorites.txt").mkdir()
assert request("POST", "/set_config", '{"AUTO_BROWSER_OPEN":true,"FAVORITES":"local/x"}')[0] == 500
assert (storage / "pldmgr_config.txt").read_bytes() == old
(storage / "favorites.txt").rmdir()
assert request("POST", "/set_config", '{"FAVORITES":"local/żółw","AUTOLOAD_DELAY":0}')[0] == 200
status, body = request("GET", "/get_config")
config = json.loads(body)
assert status == 200 and config["FAVORITES"] == "local/żółw" and config["AUTOLOAD_DELAY"] == 0
server.stdin.write("\n"); server.stdin.flush()
output, _ = server.communicate(timeout=10)
assert server.returncode == 0, output
print(output.strip())
print("HTTP real config: independent save/read, invalid input 400, I/O failure 500, retained settings and Unicode/zero passed")
finally:
if server.poll() is None: server.kill(); server.communicate()
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--ownership-only", action="store_true",
@@ -370,6 +407,7 @@ def main():
parser.add_argument("--security-only", action="store_true")
parser.add_argument("--legacy-security", action="store_true")
parser.add_argument("--serve-only", action="store_true")
parser.add_argument("--config-only", action="store_true")
parser.add_argument("--source", default="src/http_server.c",
help="Alternate HTTP source snapshot for regression reproduction")
args = parser.parse_args()
@@ -397,6 +435,8 @@ def main():
f"static const unsigned int assets_{name}_len = sizeof(assets_{name});\n")
common = compiler + ["-std=c11", "-D_DEFAULT_SOURCE", "-g", "-O1", "-Wall", "-Wextra",
"-I" + str(build), "-Iinclude", "-pthread"]
if args.config_only:
common += ["-DPLDMGR_TEST_REAL_CONFIG"]
if not args.no_sanitize:
common += ["-fsanitize=address,undefined", "-fno-omit-frame-pointer"]
cflags = [flag for flag in flags if flag.startswith("-I") or flag.startswith("-D")]
@@ -404,9 +444,12 @@ def main():
run(common + cflags + ["-include", "tests/http_test_hooks.h", "-c",
args.source, "-o", str(object_file)])
binary = build / "http_tests"
run(common + cflags + [str(object_file), "src/utils.c", "src/json_helpers.c", "tests/http_backend_stubs.c",
extra_sources = ["src/config.c"] if args.config_only else []
run(common + cflags + extra_sources + [str(object_file), "src/utils.c", "src/json_helpers.c", "tests/http_backend_stubs.c",
"tests/http_responses_test.c", "-o", str(binary)] + flags)
selected = ["--ownership-only"] if args.ownership_only else (["--oom-only"] if args.oom_only else [])
if args.config_only:
config_network_tests(binary, storage); return
if args.serve_only:
run([str(binary), "--serve"]); return
if not args.network_only and not args.security_only:
+1 -1
View File
@@ -72,7 +72,7 @@ int storage_test_rename(const char *a,const char *b) {
return rc;
}
void pldmgr_log(const char *fmt,...) { (void)fmt; }
void pldmgr_autoload_update_config_entry(const char *a,const char *b) { (void)a;(void)b;autoload_updates++; }
int pldmgr_autoload_update_config_entry(const char *a,const char *b) { (void)a;(void)b;autoload_updates++; return 0; }
/* Recorded local bytes only; any network-looking request aborts. */
int download_to_file(const char *url,const char *path) {
if (!strcmp(url, REPOSITORY_SOURCE_URL)) return -1;