diff --git a/installer/Makefile b/installer/Makefile index 90d9739..1f8f2f4 100644 --- a/installer/Makefile +++ b/installer/Makefile @@ -17,7 +17,7 @@ INTDIR := $(PROJDIR)/x64/Debug CFILES := $(wildcard $(PROJDIR)/*.c) CPPFILES := $(wildcard $(PROJDIR)/*.cpp) -OBJS := $(patsubst $(PROJDIR)/%.c, $(INTDIR)/%.o, $(CFILES)) $(patsubst $(PROJDIR)/%.cpp, $(INTDIR)/%.o, $(CPPFILES)) +OBJS := $(patsubst $(PROJDIR)/%.c, $(INTDIR)/%.o, $(CFILES)) $(patsubst $(PROJDIR)/%.cpp, $(INTDIR)/%.o, $(CPPFILES)) $(INTDIR)/jsonlite.o # llvmshim (no brew): export PATH="$HOME/llvmshim:$PATH" before make. LLVM := $(HOME)/llvmshim @@ -37,8 +37,8 @@ all: $(CONTENT_ID).pkg $(CONTENT_ID).pkg: installer/pkg.gp4 cd installer && $(TOOLCHAIN)/bin/$(CDIR)/PkgTool.Core pkg_build pkg.gp4 . && mv $(CONTENT_ID).pkg .. -installer/pkg.gp4: installer/eboot.bin installer/sce_sys/about/right.sprx installer/sce_sys/param.sfo installer/sce_sys/icon0.png $(LIBMODULES) $(ASSETS) - cd installer && $(TOOLCHAIN)/bin/$(CDIR)/create-gp4 -out pkg.gp4 --content-id=$(CONTENT_ID) --files "eboot.bin sce_sys/about/right.sprx sce_sys/param.sfo sce_sys/icon0.png sce_module/libSceFios2.prx sce_module/libc.prx $(patsubst installer/%,%,$(ASSETS))" +installer/pkg.gp4: installer/eboot.bin installer/assets/daemon.elf installer/sce_sys/about/right.sprx installer/sce_sys/param.sfo installer/sce_sys/icon0.png $(LIBMODULES) $(ASSETS) + cd installer && $(TOOLCHAIN)/bin/$(CDIR)/create-gp4 -out pkg.gp4 --content-id=$(CONTENT_ID) --files "eboot.bin assets/daemon.elf sce_sys/about/right.sprx sce_sys/param.sfo sce_sys/icon0.png sce_module/libSceFios2.prx sce_module/libc.prx $(patsubst installer/%,%,$(ASSETS))" installer/sce_sys/param.sfo: installer/Makefile $(TOOLCHAIN)/bin/$(CDIR)/PkgTool.Core sfo_new $@ @@ -60,6 +60,13 @@ installer/eboot.bin: $(OBJS) $(INTDIR)/%.o: $(PROJDIR)/%.c $(CC) $(CFLAGS) -o $@ $< +$(INTDIR)/jsonlite.o: orbisrpc/jsonlite.c + $(CC) $(CFLAGS) -o $@ $< + +# Stage the daemon payload into the app image (built first via build_sdk.sh). +installer/assets/daemon.elf: build-sdk/orbisrpc_sdk.elf + cp $< $@ + $(INTDIR)/%.o: $(PROJDIR)/%.cpp $(CCX) $(CXXFLAGS) -o $@ $< diff --git a/installer/eboot.bin b/installer/eboot.bin index e8d1b2e..84d426e 100644 Binary files a/installer/eboot.bin and b/installer/eboot.bin differ diff --git a/installer/icfg.c b/installer/icfg.c new file mode 100644 index 0000000..917d4f8 --- /dev/null +++ b/installer/icfg.c @@ -0,0 +1,158 @@ +/* icfg.c - atomic read-modify-write over the daemon config. */ +#include "icfg.h" +#include "jsonlite.h" +#include +#include +#include + +#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; } + if(fputs(s, f) < 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; + 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); + return rc == 0 ? 0 : -2; +} + +int icfg_set_str(const char *path, const char *key, const char *val){ + jl_val_t *r; + int rc; + 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); + return rc; +} + +int icfg_set_int(const char *path, const char *key, long val){ + jl_val_t *r; + int rc; + 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); + return rc; +} + +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) 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_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) 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; +} diff --git a/installer/icfg.h b/installer/icfg.h new file mode 100644 index 0000000..1a87748 --- /dev/null +++ b/installer/icfg.h @@ -0,0 +1,25 @@ +/* icfg.h - installer config read/write for /data/orbisRPC/config.json. + * Read-modify-write: preserves learned titles and every other field. + * Pure logic (token_valid) is host-tested. */ +#ifndef INSTALLER_ICFG_H +#define INSTALLER_ICFG_H +#include + +#define ICFG_PATH "/data/orbisRPC/config.json" + +/* 1 valid user token, 0 not. Tokens are dot-separated base64-ish, + * 40..150 chars. Rejects placeholders and whitespace. */ +int token_valid(const char *t); + +/* Load current token ("" when missing). 0 ok, -1 unreadable. */ +int icfg_token_load(const char *path, char *out, size_t cap); +/* Set token (validated first). 0 saved, -1 invalid, -2 I/O error. */ +int icfg_token_save(const char *path, const char *token); +/* Set a string/int key, preserving everything else. 0 saved, -1 I/O. */ +int icfg_set_str(const char *path, const char *key, const char *val); +int icfg_set_int(const char *path, const char *key, long val); +/* Read a string/int key. 0 found, -1 missing/unreadable. */ +int icfg_get_str(const char *path, const char *key, char *out, size_t cap); +int icfg_get_int(const char *path, const char *key, long *out); + +#endif diff --git a/installer/installer.c b/installer/installer.c index cfddda9..a9990c0 100644 --- a/installer/installer.c +++ b/installer/installer.c @@ -1,14 +1,154 @@ /* installer.c - orbisRPC Setup: Inject -> token -> network test -> tweaks. - * Skeleton: proves the app boots on-console with a native dialog. */ + * + * NAVIGATION LAW (from the last installer trauma): every No/cancel moves + * FORWARD to the next step. Nothing ever dumps back to the start. The only + * backward motion is none; the only exits are Done or explicit close. + */ #include +#include +#include #include "ui.h" +#include "send.h" +#include "icfg.h" +#include "nettest.h" -#define SETUP_VERSION "0.4.0" +#define DAEMON_ELF "/app0/assets/daemon.elf" + +static void step_inject_body(void){ + int port = 0; + if(ui_progress_open("Injecting orbisRPC...") != 0){ + ui_ok("Could not open progress dialog. Continuing."); + return; + } + ui_progress_set(0); + if(send_file_loopback(DAEMON_ELF, &port, ui_progress_set) == 0){ + char msg[128]; + ui_progress_close(); + snprintf(msg, sizeof msg, "Daemon injected via loader port %d.\n\nIt boots on its own from here.", port); + ui_ok(msg); + } else { + ui_progress_close(); + ui_ok("Inject failed on every loader port (9090/9021/9020).\n\nIs GoldHEN's BinLoader running? Continuing anyway."); + } +} + +static void step_inject(void){ + int q = ui_confirm("Inject the orbisRPC daemon now?\n\nSends it to the console's own loader (native, no PC)."); + if(q != 1) return; /* No -> next step, never back. */ + step_inject_body(); +} + +static void step_token(void){ + char tok[160]; + int tries, r; + int q = ui_confirm("Set your Discord token?\n\nNeeded for presence. Stored only on this console."); + if(q != 1) return; + tok[0] = 0; + icfg_token_load(ICFG_PATH, tok, sizeof tok); + for(tries = 0; tries < 3; tries++){ + r = ui_input("Discord token", "paste token, Done to save", tok, sizeof tok); + if(r < 0){ ui_ok("Text input failed. Token step skipped."); return; } + if(r == 0) return; /* cancel -> next step */ + if(token_valid(tok)){ + r = icfg_token_save(ICFG_PATH, tok); + ui_ok(r == 0 ? "Token saved." : "Could not write config. Continuing."); + return; + } + ui_ok("That doesn't look like a token.\nCheck it and try again, or cancel to skip."); + } +} + +static void step_nettest(void){ + static const struct { const char *label; const char *host; int port; } t[] = { + { "Discord gateway", "gateway.discord.gg", 443 }, + { "Discord API", "discord.com", 443 }, + { "Google DNS (internet)", "8.8.8.8", 53 }, + { "Sony TMDB (expected: blocked)", "tmdb.np.dl.playstation.net", 80 }, + }; + char out[512]; + size_t used = 0; + unsigned i; + int q = ui_confirm("Run the network test?\n\nChecks what this console can actually reach."); + if(q != 1) return; + if(ui_progress_open("Testing network...") != 0) return; + used = (size_t)snprintf(out, sizeof out, "Network test:\n"); + for(i = 0; i < sizeof t/sizeof t[0]; i++){ + int ok; + ui_progress_msg(t[i].label); + ok = net_probe(t[i].host, t[i].port, 6); + used += (size_t)snprintf(out + used, sizeof out - used, + "\n%s: %s", t[i].label, ok ? "OK" : "FAIL"); + if(used >= sizeof out - 64) break; + ui_progress_set((i + 1) * 100 / 4); + } + ui_progress_close(); + ui_ok(out); +} + +static void step_tweaks(void){ + char buf[256]; + long n; + char *end; + /* Presence text */ + if(ui_confirm("Change the idle presence text?") == 1){ + buf[0] = 0; + icfg_get_str(ICFG_PATH, "presence_state", buf, sizeof buf); + if(ui_input("Idle text", "e.g. On PS4", buf, sizeof buf) == 1 && buf[0]){ + if(icfg_set_str(ICFG_PATH, "presence_state", buf) != 0) + ui_ok("Could not save. Continuing."); + } + } + /* Poll interval */ + if(ui_confirm("Change the poll interval (seconds)?") == 1){ + if(ui_input("Poll interval", "5 to 60", buf, sizeof buf) == 1 && buf[0]){ + n = strtol(buf, &end, 10); + if(end != buf && *end == 0 && n >= 5 && n <= 60){ + if(icfg_set_int(ICFG_PATH, "poll_interval_s", n) != 0) + ui_ok("Could not save. Continuing."); + } else { + ui_ok("Must be a number 5..60. Keeping current."); + } + } + } + /* Home art */ + if(ui_confirm("Change the idle tile art?") == 1){ + buf[0] = 0; + icfg_get_str(ICFG_PATH, "home_art", buf, sizeof buf); + if(ui_input("Idle art", "URL, asset key, or empty", buf, sizeof buf) == 1){ + if(icfg_set_str(ICFG_PATH, "home_art", buf) != 0) + ui_ok("Could not save. Continuing."); + } + } + /* Debug logging */ + { + int d = ui_confirm("Turn debug logging on?"); + if(d == 1) icfg_set_int(ICFG_PATH, "debug", 1); + else if(d == 0) icfg_set_int(ICFG_PATH, "debug", 0); + } + ui_ok("Settings saved (where edited)."); +} int main(void){ - char msg[256]; - snprintf(msg, sizeof msg, - "orbisRPC Setup %s\n\nInstaller skeleton boots.\nWizard lands next.", SETUP_VERSION); - ui_ok(msg); + char tok[160]; + int first_run; + tok[0] = 0; + icfg_token_load(ICFG_PATH, tok, sizeof tok); + first_run = !token_valid(tok); + if(!first_run){ + /* Returning user: inject? no -> settings? no -> exit. Forward only. */ + if(ui_confirm("orbisRPC Setup.\n\nInject the daemon?") == 1){ + step_inject_body(); + ui_ok("Done."); + return 0; + } + if(ui_confirm("Open settings instead?") == 1) + step_tweaks(); + return 0; + } + /* First run wizard: every No skips forward. */ + step_inject(); + step_token(); + step_nettest(); + ui_ok("Setup complete.\n\nLaunch a game and watch Discord."); return 0; } diff --git a/installer/nettest.c b/installer/nettest.c new file mode 100644 index 0000000..abe6be7 --- /dev/null +++ b/installer/nettest.c @@ -0,0 +1,40 @@ +/* nettest.c - connect-only probes. A closed port and a filtered host + * both read as unreachable; that honesty is the point (Sony TMDB from + * the console genuinely fails while Discord succeeds). */ +#include "nettest.h" +#include +#include +#include +#include +#include +#include +#include +#include + +int net_probe(const char *host, int port, int timeout_s){ + struct addrinfo hints, *res = NULL, *rp; + char svc[8]; + int ok = 0; + if(!host || !host[0] || port <= 0 || port > 65535) return 0; + if(timeout_s < 1) timeout_s = 1; + if(timeout_s > 10) timeout_s = 10; + snprintf(svc, sizeof svc, "%d", port); + memset(&hints, 0, sizeof hints); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + if(getaddrinfo(host, svc, &hints, &res) != 0 || !res) return 0; + for(rp = res; rp; rp = rp->ai_next){ + int fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + struct timeval tv; + if(fd < 0) continue; + tv.tv_sec = timeout_s; + tv.tv_usec = 0; + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv); + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); + if(connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) ok = 1; + close(fd); + if(ok) break; + } + freeaddrinfo(res); + return ok; +} diff --git a/installer/nettest.h b/installer/nettest.h new file mode 100644 index 0000000..32054a5 --- /dev/null +++ b/installer/nettest.h @@ -0,0 +1,8 @@ +/* nettest.h - honest TCP reachability probes (no traffic beyond connect). */ +#ifndef INSTALLER_NETTEST_H +#define INSTALLER_NETTEST_H + +/* 1 reachable, 0 not. timeout_s clamped 1..10. Host-testable. */ +int net_probe(const char *host, int port, int timeout_s); + +#endif diff --git a/installer/pkg.gp4 b/installer/pkg.gp4 index 667a500..44309ea 100644 --- a/installer/pkg.gp4 +++ b/installer/pkg.gp4 @@ -3,7 +3,7 @@ pkg_ps4_app PS4VOLUME - 2026-09-24 16:29:15 + 2026-09-24 16:43:31 @@ -17,6 +17,7 @@ + diff --git a/installer/send.c b/installer/send.c new file mode 100644 index 0000000..fe03053 --- /dev/null +++ b/installer/send.c @@ -0,0 +1,73 @@ +/* send.c - loopback payload injection: file bytes over TCP to the + * console's own loaders. Chunked send (no whole-file malloc). */ +#include "send.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define SEND_CHUNK (64u*1024u) +static const int SEND_PORTS[] = { 9090, 9021, 9020 }; + +static int send_one(int fd, const char *path, void (*progress)(unsigned)){ + FILE *f = fopen(path, "rb"); + unsigned char buf[SEND_CHUNK]; + long total = 0, sent = 0; + size_t n; + if(!f) return -1; + if(fseek(f, 0, SEEK_END) != 0){ fclose(f); return -1; } + total = ftell(f); + if(total <= 0 || total > 16*1024*1024){ fclose(f); return -1; } + if(fseek(f, 0, SEEK_SET) != 0){ fclose(f); return -1; } + if(progress) progress(0); + while((n = fread(buf, 1, sizeof buf, f)) > 0){ + size_t off = 0; + while(off < n){ + ssize_t w = send(fd, buf + off, n - off, 0); + if(w <= 0){ + if(errno == EAGAIN || errno == EWOULDBLOCK) continue; + fclose(f); + return -1; + } + off += (size_t)w; + } + sent += (long)n; + if(progress) progress((unsigned)(sent * 100 / total)); + } + fclose(f); + if(progress) progress(100); + return 0; +} + +int send_file_loopback(const char *path, int *port_used, void (*progress)(unsigned)){ + unsigned i; + if(!path || !path[0]) return -1; + for(i = 0; i < sizeof SEND_PORTS/sizeof SEND_PORTS[0]; i++){ + int fd = socket(AF_INET, SOCK_STREAM, 0); + struct sockaddr_in sa; + struct timeval tv = { 5, 0 }; + if(fd < 0) continue; + setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv); + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); + memset(&sa, 0, sizeof sa); + sa.sin_family = AF_INET; + sa.sin_port = htons((uint16_t)SEND_PORTS[i]); + sa.sin_addr.s_addr = inet_addr("127.0.0.1"); + if(connect(fd, (struct sockaddr *)&sa, sizeof sa) < 0){ + close(fd); + continue; + } + if(send_one(fd, path, progress) == 0){ + close(fd); + if(port_used) *port_used = SEND_PORTS[i]; + return 0; + } + close(fd); + } + return -1; +} diff --git a/installer/send.h b/installer/send.h new file mode 100644 index 0000000..8c8ccc4 --- /dev/null +++ b/installer/send.h @@ -0,0 +1,10 @@ +/* send.h - loopback payload injection (clean-room, standard sockets). + * Tries native GoldHEN BinLoader 9090, then elfldr 9021, then 9020. + * 9020 is one-shot: accepted bytes count as sent. */ +#ifndef INSTALLER_SEND_H +#define INSTALLER_SEND_H + +/* progress(pct) may be NULL. Returns 0 + port_used set on success. */ +int send_file_loopback(const char *path, int *port_used, void (*progress)(unsigned pct)); + +#endif diff --git a/installer/ui.c b/installer/ui.c index adb14bc..9e79350 100644 --- a/installer/ui.c +++ b/installer/ui.c @@ -4,9 +4,13 @@ #include "ui.h" #include #include +#include #include #include +#include +#include #include +#include static void base_init(OrbisMsgDialogParam *param){ memset(param, 0, sizeof(*param)); @@ -14,6 +18,7 @@ static void base_init(OrbisMsgDialogParam *param){ param->baseParam.magic = (uint32_t)(ORBIS_COMMON_DIALOG_MAGIC_NUMBER + (uint64_t)¶m->baseParam); param->size = sizeof(OrbisMsgDialogParam); + param->mode = ORBIS_MSG_DIALOG_MODE_USER_MSG; } int ui_ok(const char *msg){ @@ -53,3 +58,85 @@ int ui_confirm(const char *msg){ sceMsgDialogTerminate(); return res.buttonId == ORBIS_MSG_DIALOG_BUTTON_ID_YES ? 1 : 0; } + +static int progress_open = 0; + +int ui_progress_open(const char *msg){ + OrbisMsgDialogParam param; + OrbisMsgDialogProgressBarParam bar; + if(progress_open) return 0; + sceMsgDialogInitialize(); + base_init(¶m); + param.mode = ORBIS_MSG_DIALOG_MODE_PROGRESS_BAR; + memset(&bar, 0, sizeof bar); + bar.barType = ORBIS_MSG_DIALOG_PROGRESSBAR_TYPE_PERCENTAGE; + bar.msg = msg; + param.progBarParam = &bar; + if(sceMsgDialogOpen(¶m) < 0){ sceMsgDialogTerminate(); return -1; } + progress_open = 1; + return 0; +} + +void ui_progress_msg(const char *msg){ + if(progress_open && msg) sceMsgDialogProgressBarSetMsg(0, msg); +} + +void ui_progress_set(unsigned pct){ + if(progress_open) sceMsgDialogProgressBarSetValue(0, pct > 100 ? 100 : pct); +} + +void ui_progress_close(void){ + if(!progress_open) return; + progress_open = 0; + sceMsgDialogClose(); + sceMsgDialogTerminate(); +} + +int ui_input(const char *title, const char *placeholder, char *out, size_t cap){ + static wchar_t wbuf[256]; + static wchar_t wtitle[64]; + static wchar_t wplace[64]; + OrbisImeDialogSetting st; + OrbisDialogResult res; + int32_t uid = 0; + size_t i; + if(!out || cap == 0 || cap > 200) return -1; + /* Prefill with current value (ASCII round-trip). */ + for(i = 0; i < cap - 1 && out[i]; i++) wbuf[i] = (wchar_t)(unsigned char)out[i]; + wbuf[i] = 0; + for(i = 0; i < 63 && title && title[i]; i++) wtitle[i] = (wchar_t)(unsigned char)title[i]; + wtitle[i] = 0; + for(i = 0; i < 63 && placeholder && placeholder[i]; i++) wplace[i] = (wchar_t)(unsigned char)placeholder[i]; + wplace[i] = 0; + if(sceUserServiceGetInitialUser(&uid) < 0) uid = 0; + memset(&st, 0, sizeof st); + st.userId = (uint32_t)uid; + st.type = 0; + st.supportedLanguages = 0; + st.enterLabel = ORBIS_BUTTON_LABEL_DEFAULT; + st.inputMethod = 0; + st.filter = 0; + st.option = 0; + st.maxTextLength = (uint32_t)(cap - 1); + st.inputTextBuffer = wbuf; + st.posx = 0; + st.posy = 0; + st.horizontalAlignment = ORBIS_H_CENTER; + st.verticalAlignment = ORBIS_V_CENTER; + st.placeholder = wplace; + st.title = wtitle; + sceImeDialogInit(&st, NULL); + /* Pump until the OSK stops (IME uses its own status enum). */ + while(sceImeDialogGetStatus() != ORBIS_DIALOG_STATUS_STOPPED){ + /* The dialog system needs no explicit pump; yield briefly. */ + sceKernelUsleep(20000); + } + memset(&res, 0, sizeof res); + if(sceImeDialogGetResult(&res) < 0){ sceImeDialogTerm(); return -1; } + sceImeDialogTerm(); + if(res.endstatus != ORBIS_DIALOG_OK) return 0; + for(i = 0; i < cap - 1 && wbuf[i]; i++) + out[i] = (wbuf[i] < 128 && wbuf[i] >= 32) ? (char)wbuf[i] : '?'; + out[i] = 0; + return 1; +} diff --git a/installer/ui.h b/installer/ui.h index 7b9c165..e77e873 100644 --- a/installer/ui.h +++ b/installer/ui.h @@ -1,10 +1,21 @@ /* ui.h - native dialog helpers for the installer wizard. */ #ifndef INSTALLER_UI_H #define INSTALLER_UI_H +#include /* Info dialog with OK. 0 shown, -1 failed to open. */ int ui_ok(const char *msg); /* Yes/No dialog, focus on No (safe default). 1 yes, 0 no/closed, -1 error. */ int ui_confirm(const char *msg); +/* Progress dialog. Open once, update, close. 0 ok, -1 error. */ +int ui_progress_open(const char *msg); +void ui_progress_msg(const char *msg); +void ui_progress_set(unsigned pct); +void ui_progress_close(void); + +/* OSK text input. Prefills with out's current content. Returns 1 + out + * filled on Done, 0 on cancel, -1 on error. */ +int ui_input(const char *title, const char *placeholder, char *out, size_t cap); + #endif diff --git a/tests/Makefile b/tests/Makefile index caf38f8..fcf05e5 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -9,18 +9,21 @@ MBEDTLS_DIR := ../third_party/mbedtls/library MBEDTLS_SRCS := $(filter-out $(MBEDTLS_DIR)/net_sockets.c $(MBEDTLS_DIR)/timing.c $(MBEDTLS_DIR)/entropy_poll.c,$(wildcard $(MBEDTLS_DIR)/*.c)) ORBIS_SRCS := ../orbisrpc/jsonlite.c ../orbisrpc/b64.c ../orbisrpc/sfo.c ../orbisrpc/tmdb_crypto.c ../orbisrpc/updater_util.c ../orbisrpc/art.c ../orbisrpc/log.c ../orbisrpc/health.c ../orbisrpc/manifest.c ../orbisrpc/compat.c ../orbisrpc/cfg.c ../orbisrpc/appdb.c ../orbisrpc/discord.c +INST_SRCS := ../installer/icfg.c ../installer/nettest.c # SQLite amalgamation: -O0 for host iteration speed (payload uses -O2). SQLITE_DIR := ../third_party/sqlite/sqlite-amalgamation-3510100 SQLITE_FLAGS := -O0 -DSQLITE_THREADSAFE=0 -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_OMIT_DEPRECATED -I$(SQLITE_DIR) ORBIS_OBJS := $(patsubst ../orbisrpc/%.c,$(OBJDIR)/orbis_%.o,$(ORBIS_SRCS)) +INST_OBJS := $(patsubst ../installer/%.c,$(OBJDIR)/inst_%.o,$(INST_SRCS)) MBEDTLS_OBJS := $(patsubst $(MBEDTLS_DIR)/%.c,$(OBJDIR)/mbed_%.o,$(MBEDTLS_SRCS)) -TEST_OBJS := $(OBJDIR)/test_utils.o $(ORBIS_OBJS) $(MBEDTLS_OBJS) $(OBJDIR)/sqlite3.o +TEST_OBJS := $(OBJDIR)/test_utils.o $(ORBIS_OBJS) $(INST_OBJS) $(MBEDTLS_OBJS) $(OBJDIR)/sqlite3.o ASAN_FLAGS := -fsanitize=address,undefined -fno-omit-frame-pointer ASAN_ORBIS_OBJS := $(patsubst ../orbisrpc/%.c,$(ASAN_OBJDIR)/orbis_%.o,$(ORBIS_SRCS)) +ASAN_INST_OBJS := $(patsubst ../installer/%.c,$(ASAN_OBJDIR)/inst_%.o,$(INST_SRCS)) ASAN_MBEDTLS_OBJS := $(patsubst $(MBEDTLS_DIR)/%.c,$(ASAN_OBJDIR)/mbed_%.o,$(MBEDTLS_SRCS)) -ASAN_OBJS := $(ASAN_OBJDIR)/test_utils.o $(ASAN_ORBIS_OBJS) $(ASAN_MBEDTLS_OBJS) $(ASAN_OBJDIR)/sqlite3.o +ASAN_OBJS := $(ASAN_OBJDIR)/test_utils.o $(ASAN_ORBIS_OBJS) $(ASAN_INST_OBJS) $(ASAN_MBEDTLS_OBJS) $(ASAN_OBJDIR)/sqlite3.o .PHONY: all clean test asan @@ -48,6 +51,9 @@ $(OBJDIR)/test_utils.o: test_utils.c | $(OBJDIR) $(OBJDIR)/orbis_%.o: ../orbisrpc/%.c | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< +$(OBJDIR)/inst_%.o: ../installer/%.c | $(OBJDIR) + $(CC) $(CFLAGS) -I../installer -c -o $@ $< + $(OBJDIR)/mbed_%.o: $(MBEDTLS_DIR)/%.c | $(OBJDIR) $(CC) $(CFLAGS) -Wno-unused-parameter -c -o $@ $< @@ -63,6 +69,9 @@ $(ASAN_OBJDIR)/test_utils.o: test_utils.c | $(ASAN_OBJDIR) $(ASAN_OBJDIR)/orbis_%.o: ../orbisrpc/%.c | $(ASAN_OBJDIR) $(CC) $(CFLAGS) $(ASAN_FLAGS) -c -o $@ $< +$(ASAN_OBJDIR)/inst_%.o: ../installer/%.c | $(ASAN_OBJDIR) + $(CC) $(CFLAGS) $(ASAN_FLAGS) -I../installer -c -o $@ $< + $(ASAN_OBJDIR)/mbed_%.o: $(MBEDTLS_DIR)/%.c | $(ASAN_OBJDIR) $(CC) $(CFLAGS) $(ASAN_FLAGS) -Wno-unused-parameter -c -o $@ $< diff --git a/tests/test_utils.c b/tests/test_utils.c index 89791cf..2eb0310 100644 --- a/tests/test_utils.c +++ b/tests/test_utils.c @@ -10,6 +10,8 @@ #include "../orbisrpc/appdb.h" #include "../orbisrpc/discord.h" #include "../orbisrpc/detect.h" +#include "../installer/icfg.h" +#include "../installer/nettest.h" #include "sqlite3.h" #include @@ -583,6 +585,39 @@ static void test_cfg_learn(void) { assert(cfg_learn(&c, "CUSA00001", "Full Game") == 0); } +static void test_installer_cfg(void) { + char dir[64], path[96]; + assert(make_tmpdir(dir, sizeof dir) == 0); + snprintf(path, sizeof path, "%s/config.json", dir); + /* token validation */ + assert(token_valid("MTIz.ABCdef_0123456789-xYz.ABCDEFGHijklmnopQRSTUVwxyz") == 1); + assert(token_valid("short") == 0); + assert(token_valid("SET_ME") == 0); + assert(token_valid("has space.in.it.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") == 0); + assert(token_valid("nodots") == 0); + assert(token_valid(NULL) == 0); + /* save creates, preserves, round-trips */ + assert(icfg_token_save(path, "MTIz.ABCdef_0123456789-xYz.ABCDEFGHijklmnopQRSTUVwxyz") == 0); + assert(icfg_set_str(path, "presence_state", "On PS4") == 0); + assert(icfg_set_int(path, "poll_interval_s", 12) == 0); + /* invalid token refused, file untouched */ + assert(icfg_token_save(path, "junk") == -1); + char tok[160], st[64]; + long n = 0; + assert(icfg_token_load(path, tok, sizeof tok) == 0); + assert(!strcmp(tok, "MTIz.ABCdef_0123456789-xYz.ABCDEFGHijklmnopQRSTUVwxyz")); + assert(icfg_get_str(path, "presence_state", st, sizeof st) == 0); + assert(!strcmp(st, "On PS4")); + assert(icfg_get_int(path, "poll_interval_s", &n) == 0 && n == 12); + /* missing file: loads fail soft, save still works */ + assert(icfg_get_str("/nonexistent/x.json", "k", st, sizeof st) != 0); + /* net probes fail soft on garbage */ + assert(net_probe(NULL, 443, 2) == 0); + assert(net_probe("", 443, 2) == 0); + assert(net_probe("127.0.0.1", 1, 1) == 0); + assert(net_probe("nonexistent.invalid", 443, 1) == 0); +} + int main(void) { test_json(); test_gateway_op_spoof(); @@ -599,6 +634,7 @@ int main(void) { test_manifest_sig(); test_cfg_titles(); test_cfg_learn(); + test_installer_cfg(); test_appdb(); test_discord_builder(); puts("utility tests passed");