mirror of
https://github.com/SirHumza/orbisRPC.git
synced 2026-10-06 09:00:37 +02:00
- ui.c: fullscreen Discord-blurple cards, embedded bitmap font, pad X-to-continue, progress bar; IME token entry kept as overlay - scripts/genfont.py + installer/font.h: build-time rasterized font - Makefile: -lSDL2 -lSDL2main -lScePigletv2VSH -lSceAudioOut -lSceVideoOut, font.h regen rule; drop stock -lSceMsgDialog - daemon: gateway state machine, status.json heartbeat, gen supersede, no-exit token wait; updater removed per direction - installer: config preserve on reinstall, honest stage logging, token-save verification, daemon liveness on done screen
248 lines
6.8 KiB
C
248 lines
6.8 KiB
C
/* icfg.c - atomic read-modify-write over the daemon config. */
|
|
#include "icfg.h"
|
|
#include "jsonlite.h"
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
|
|
#define ICFG_MAX (64u*1024u)
|
|
|
|
int token_valid(const char *t){
|
|
size_t n, i;
|
|
int dots = 0;
|
|
if(!t) return 0;
|
|
n = strlen(t);
|
|
if(n < 40 || n > 150) return 0;
|
|
if(!strcmp(t, "SET_ME") || !strcmp(t, "PUT_TOKEN_HERE")) return 0;
|
|
for(i = 0; i < n; i++){
|
|
char c = t[i];
|
|
if(c == '.'){ dots++; continue; }
|
|
if(!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
|
(c >= '0' && c <= '9') || c == '_' || c == '-'))
|
|
return 0;
|
|
}
|
|
return dots >= 2;
|
|
}
|
|
|
|
static jl_val_t *icfg_read(const char *path){
|
|
FILE *f = fopen(path, "rb");
|
|
long sz;
|
|
char *buf;
|
|
jl_val_t *r;
|
|
if(!f) return NULL;
|
|
if(fseek(f, 0, SEEK_END) != 0){ fclose(f); return NULL; }
|
|
sz = ftell(f);
|
|
if(sz <= 0 || sz > (long)ICFG_MAX){ fclose(f); return NULL; }
|
|
if(fseek(f, 0, SEEK_SET) != 0){ fclose(f); return NULL; }
|
|
buf = (char *)malloc((size_t)sz + 1);
|
|
if(!buf){ fclose(f); return NULL; }
|
|
if(fread(buf, 1, (size_t)sz, f) != (size_t)sz){
|
|
free(buf); fclose(f); return NULL;
|
|
}
|
|
fclose(f);
|
|
buf[sz] = 0;
|
|
r = jl_parse(buf, (size_t)sz);
|
|
free(buf);
|
|
if(!r || r->type != JL_OBJECT){ if(r) jl_free(r); return NULL; }
|
|
return r;
|
|
}
|
|
|
|
static int icfg_write(const char *path, jl_val_t *r){
|
|
char tmp[300];
|
|
char *s;
|
|
FILE *f;
|
|
int ok = 1;
|
|
if(snprintf(tmp, sizeof tmp, "%s.new", path) <= 0 ||
|
|
(size_t)strlen(path) + 5 >= sizeof tmp)
|
|
return -1;
|
|
s = jl_stringify(r);
|
|
if(!s) return -1;
|
|
f = fopen(tmp, "wb");
|
|
if(!f){ free(s); return -1; }
|
|
/* token lives in this file: owner-only before bytes hit disk */
|
|
{ int fd0 = fileno(f); if(fd0 >= 0) fchmod(fd0, 0600); }
|
|
if(fputs(s, f) < 0) ok = 0;
|
|
if(ok){
|
|
int fd = fileno(f);
|
|
if(fd >= 0 && fsync(fd) != 0) ok = 0;
|
|
}
|
|
if(ok && fclose(f) != 0) ok = 0;
|
|
free(s);
|
|
if(!ok){ remove(tmp); return -1; }
|
|
if(rename(tmp, path) != 0){ remove(tmp); return -1; }
|
|
return 0;
|
|
}
|
|
|
|
int icfg_token_load(const char *path, char *out, size_t cap){
|
|
jl_val_t *r = icfg_read(path);
|
|
const jl_val_t *v;
|
|
if(!out || cap == 0) return -1;
|
|
out[0] = 0;
|
|
if(!r) return -1;
|
|
v = jl_obj_get(r, "token");
|
|
if(v && v->type == JL_STRING && v->str){
|
|
strncpy(out, v->str, cap - 1);
|
|
out[cap - 1] = 0;
|
|
}
|
|
jl_free(r);
|
|
return 0;
|
|
}
|
|
|
|
int icfg_token_save(const char *path, const char *token){
|
|
jl_val_t *r;
|
|
int rc;
|
|
char back[160];
|
|
if(!token_valid(token)) return -1;
|
|
r = icfg_read(path);
|
|
if(!r){
|
|
r = jl_new_object();
|
|
if(!r) return -2;
|
|
}
|
|
jl_obj_set(r, "token", jl_new_string(token));
|
|
rc = icfg_write(path, r);
|
|
jl_free(r);
|
|
if(rc != 0) return -2;
|
|
/* Read-back proof: an OOM inside the JSON build saves silently
|
|
* WITHOUT the key. Never report success on faith. */
|
|
back[0] = 0;
|
|
if(icfg_token_load(path, back, sizeof back) != 0 ||
|
|
strcmp(back, token) != 0)
|
|
return -2;
|
|
return 0;
|
|
}
|
|
|
|
int icfg_set_str(const char *path, const char *key, const char *val){
|
|
jl_val_t *r;
|
|
int rc;
|
|
char back[256];
|
|
if(!key || !key[0] || !val) return -1;
|
|
r = icfg_read(path);
|
|
if(!r){
|
|
r = jl_new_object();
|
|
if(!r) return -1;
|
|
}
|
|
jl_obj_set(r, key, jl_new_string(val));
|
|
rc = icfg_write(path, r);
|
|
jl_free(r);
|
|
if(rc != 0) return -1;
|
|
back[0] = 0;
|
|
if(icfg_get_str(path, key, back, sizeof back) != 0 ||
|
|
strcmp(back, val) != 0)
|
|
return -1;
|
|
return 0;
|
|
}
|
|
|
|
int icfg_set_int(const char *path, const char *key, long val){
|
|
jl_val_t *r;
|
|
int rc;
|
|
long back = 0;
|
|
if(!key || !key[0]) return -1;
|
|
r = icfg_read(path);
|
|
if(!r){
|
|
r = jl_new_object();
|
|
if(!r) return -1;
|
|
}
|
|
jl_obj_set(r, key, jl_new_int(val));
|
|
rc = icfg_write(path, r);
|
|
jl_free(r);
|
|
if(rc != 0) return -1;
|
|
if(icfg_get_int(path, key, &back) != 0 || back != val) return -1;
|
|
return 0;
|
|
}
|
|
|
|
int icfg_get_str(const char *path, const char *key, char *out, size_t cap){
|
|
jl_val_t *r = icfg_read(path);
|
|
const jl_val_t *v;
|
|
if(!out || cap == 0) return -1;
|
|
out[0] = 0;
|
|
if(!r || !key){ if(r) jl_free(r); return -1; }
|
|
v = jl_obj_get(r, key);
|
|
if(v && v->type == JL_STRING && v->str){
|
|
strncpy(out, v->str, cap - 1);
|
|
out[cap - 1] = 0;
|
|
}
|
|
jl_free(r);
|
|
return 0;
|
|
}
|
|
|
|
int icfg_titles_count(const char *path){
|
|
jl_val_t *r = icfg_read(path);
|
|
const jl_val_t *t;
|
|
int n = 0;
|
|
if(!r) return 0;
|
|
t = jl_obj_get(r, "titles");
|
|
if(t && t->type == JL_OBJECT){
|
|
for(jl_val_t *p = t->child; p; p = p->next) n++;
|
|
}
|
|
jl_free(r);
|
|
return n;
|
|
}
|
|
|
|
int icfg_get_int(const char *path, const char *key, long *out){
|
|
jl_val_t *r = icfg_read(path);
|
|
const jl_val_t *v;
|
|
if(!out) return -1;
|
|
if(!r || !key){ if(r) jl_free(r); return -1; }
|
|
v = jl_obj_get(r, key);
|
|
if(v && v->type == JL_NUMBER){
|
|
*out = v->num_is_int ? (long)v->inum : (long)v->num;
|
|
jl_free(r);
|
|
return 0;
|
|
}
|
|
jl_free(r);
|
|
return -1;
|
|
}
|
|
|
|
/* Daemon liveness from status.json (the daemon's alive tick writes
|
|
* {"version","state","title","ts"}). now = time(NULL) at the caller,
|
|
* max_age_s = freshness bound (120 covers the 60s tick plus slack).
|
|
* Returns 1 fresh (state_out gets "state", "" when absent),
|
|
* 0 missing/corrupt/stale/unreadable. Never crashes on garbage. */
|
|
int icfg_daemon_state(const char *path, long now, long max_age_s,
|
|
char *state_out, size_t state_cap){
|
|
FILE *f;
|
|
long sz;
|
|
char buf[1024];
|
|
size_t n;
|
|
const char *p, *q;
|
|
long ts;
|
|
if(state_out && state_cap) state_out[0] = 0;
|
|
if(!path || max_age_s < 0) return 0;
|
|
f = fopen(path, "rb");
|
|
if(!f) return 0;
|
|
if(fseek(f, 0, SEEK_END) != 0){ fclose(f); return 0; }
|
|
sz = ftell(f);
|
|
if(sz <= 0 || sz >= (long)sizeof buf){ fclose(f); return 0; }
|
|
if(fseek(f, 0, SEEK_SET) != 0){ fclose(f); return 0; }
|
|
n = fread(buf, 1, (size_t)sz, f);
|
|
fclose(f);
|
|
if(n != (size_t)sz) return 0;
|
|
buf[n] = 0;
|
|
p = strstr(buf, "\"ts\"");
|
|
if(!p) return 0;
|
|
p = strchr(p + 4, ':');
|
|
if(!p) return 0;
|
|
ts = strtol(p + 1, NULL, 10);
|
|
if(ts <= 0 || ts > now || now - ts > max_age_s) return 0;
|
|
p = strstr(buf, "\"state\"");
|
|
if(p){
|
|
p = strchr(p + 7, ':');
|
|
if(p){
|
|
p = strchr(p + 1, '"');
|
|
if(p){
|
|
p++;
|
|
q = strchr(p, '"');
|
|
if(q && state_out && state_cap){
|
|
size_t len = (size_t)(q - p);
|
|
if(len >= state_cap) len = state_cap - 1;
|
|
memcpy(state_out, p, len);
|
|
state_out[len] = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return 1;
|
|
}
|