From fcd43b54ae7581c91e26486ddc1f9f32e4390e3c Mon Sep 17 00:00:00 2001 From: Knutwurst <36196269+knutwurst@users.noreply.github.com> Date: Wed, 24 Jun 2026 21:50:59 +0200 Subject: [PATCH] Home-screen tile via sceAppInstUtilAppInstallTitleDir MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Write param.json + icon0.png into /user/app//sce_sys/, then call sceAppInstUtilAppInstallTitleDir to register the directory as an app. No package, no code signing — the installer reads the metadata files directly. The tile uses TITLE_ID PTDL00001 and deeplinkUri http://127.0.0.1:12880/, so tapping it opens PatchDL's own UI in the on-console browser. Only useful while the ELF is running. Asset pipeline: param.json + icon0.png live under assets/ and get .incbin'd straight into .rodata. Makefile lists them as TILE_ASSETS so a touch on either forces a relink. Stat-guard: file_matches() diffs each asset against the on-disk copy first; when nothing changed the install API isn't called at all. Keeps repeated payload starts from re-registering the app. Repeated /api/install_tile calls return "already installed and up to date". Backend lazy-load: AppInstUtil isn't mapped until something pokes it, so the helper polls patchdl_install_backend_check() for up to ~15 s before resolving sceAppInstUtilAppInstallTitleDir. Wiring: /api/install_tile POST triggers the install on demand; patchdl_websrv_start() runs it at startup when home_shortcut is on; and flipping the toggle from off to on in /api/config also fires it. All three paths share the stat-guarded helper so they're safe to repeat. --- Makefile | 11 ++- assets/icon0.png | Bin 0 -> 21476 bytes assets/icon0.svg | 20 +++++ assets/param.json | 10 +++ src/patchdl_tile.c | 205 +++++++++++++++++++++++++++++++++++++++++++ src/patchdl_tile.h | 23 +++++ src/patchdl_websrv.c | 38 +++++++- 7 files changed, 302 insertions(+), 5 deletions(-) create mode 100644 assets/icon0.png create mode 100644 assets/icon0.svg create mode 100644 assets/param.json create mode 100644 src/patchdl_tile.c create mode 100644 src/patchdl_tile.h diff --git a/Makefile b/Makefile index 07c8d78..d0e05fa 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,12 @@ SRCS := src/main.c \ src/patchdl_resolve.c \ src/patchdl_verxml.c \ src/patchdl_install.c \ - src/patchdl_notify.c + src/patchdl_notify.c \ + src/patchdl_tile.c + +# patchdl_tile.c uses .incbin to embed param.json + icon0.png; touching the +# assets must trigger a rebuild. +TILE_ASSETS := assets/param.json assets/icon0.png WEB_ASSETS := web/index.html web/styles.css web/app.js GEN_SRCS := $(patsubst web/%,gen/web/%.c,$(WEB_ASSETS)) @@ -56,8 +61,8 @@ gen/web/%.c: web/% scripts/gen_asset_module.py | gen/web $(SQLITE_OBJ): $(SQLITE_DIR)/sqlite3.c $(CC) $(SQLITE_CFLAGS) -c -o $@ $< -$(BIN): $(SRCS) $(GEN_SRCS) $(SQLITE_OBJ) - $(CC) $(CFLAGS) -o $@ $^ $(LDADD) +$(BIN): $(SRCS) $(GEN_SRCS) $(SQLITE_OBJ) $(TILE_ASSETS) + $(CC) $(CFLAGS) -o $@ $(SRCS) $(GEN_SRCS) $(SQLITE_OBJ) $(LDADD) test: $(BIN) $(PS5_DEPLOY) -h $(PS5_HOST) -p $(PS5_PORT) $^ diff --git a/assets/icon0.png b/assets/icon0.png new file mode 100644 index 0000000000000000000000000000000000000000..4f2f066bcefc0051f10ed8c3b54e38b9230fc6b2 GIT binary patch literal 21476 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 + + + + + + + + + + + + + PD + PatchDL + PS5 Patch Tool + diff --git a/assets/param.json b/assets/param.json new file mode 100644 index 0000000..560f7bf --- /dev/null +++ b/assets/param.json @@ -0,0 +1,10 @@ +{ + "titleId": "PTDL00001", + "deeplinkUri": "http://127.0.0.1:12880/", + "localizedParameters": { + "defaultLanguage": "en-US", + "en-US": { + "titleName": "PatchDL" + } + } +} diff --git a/src/patchdl_tile.c b/src/patchdl_tile.c new file mode 100644 index 0000000..b6a64d3 --- /dev/null +++ b/src/patchdl_tile.c @@ -0,0 +1,205 @@ +#include "patchdl_tile.h" +#include "patchdl_install.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +/* Embed the tile assets into .rodata directly via .incbin — no codegen step, + no Python helper, no second translation unit. Matches itsPLK's pattern. */ +#define INCASSET(name, file) \ + __asm__(".section .rodata\n" \ + ".global " #name "\n" \ + ".global " #name "_end\n" \ + ".global " #name "_size\n" \ + ".align 16\n" #name ":\n" \ + ".incbin \"" file "\"\n" #name "_end:\n" #name "_size:\n" \ + ".quad " #name "_end - " #name "\n" \ + ".previous\n"); \ + extern const uint8_t name[]; \ + extern const size_t name##_size; + +INCASSET(tile_param_json, "assets/param.json"); +INCASSET(tile_icon0_png, "assets/icon0.png"); + +#define TILE_TITLE_ID "PTDL00001" + +/* Forward decls — we resolve sceAppInstUtilInitialize/AppInstallTitleDir at + runtime via the kernel dynlib helpers so the ELF stays loader-friendly. */ +typedef int (*ai_init_fn)(void); +typedef int (*ai_install_dir_fn)(const char *title_id, const char *parent_dir, + void *opts); + +static intptr_t +dynsym_by_name(const char *sym) { + uint32_t h = 0; + if (kernel_dynlib_handle(-1, "libSceAppInstUtil.sprx", &h) < 0) return 0; + return kernel_dynlib_dlsym(-1, h, sym); +} + +static intptr_t +dynsym_by_nid(const char *nid) { + uint32_t h = 0; + if (kernel_dynlib_handle(-1, "libSceAppInstUtil.sprx", &h) < 0) return 0; + return kernel_dynlib_resolve(-1, h, nid); +} + +static int +write_all(const char *path, const uint8_t *data, size_t size) { + int fd; + ssize_t w; + size_t off = 0; + + /* O_NOFOLLOW + 0600: don't follow a symlink at the destination, and don't + create the file with the libc default 0666 mode. Same pattern as the + net layer's fopen_safe. */ + fd = open(path, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW | O_CLOEXEC, 0600); + if (fd < 0) return -1; + while (off < size) { + w = write(fd, data + off, size - off); + if (w < 0) { + if (errno == EINTR) continue; + close(fd); + return -1; + } + off += (size_t)w; + } + close(fd); + return 0; +} + +static int +file_matches(const char *path, const uint8_t *expected, size_t expected_size) { + struct stat st; + uint8_t *buf; + int fd; + ssize_t n; + int match = 0; + + if (stat(path, &st) != 0) return 0; + if ((size_t)st.st_size != expected_size) return 0; + + fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) return 0; + + buf = malloc(expected_size); + if (!buf) { close(fd); return 0; } + + n = read(fd, buf, expected_size); + close(fd); + if (n == (ssize_t)expected_size && memcmp(buf, expected, expected_size) == 0) + match = 1; + + free(buf); + return match; +} + +int +patchdl_tile_install_if_needed(char *msg, size_t msg_sz) { + char base_dir[128]; + char sce_sys_dir[160]; + char param_path[192]; + char icon_path[192]; + + ai_init_fn ai_initialize = NULL; + ai_install_dir_fn ai_install_title = NULL; + int rc; + + snprintf(base_dir, sizeof base_dir, "/user/app/%s", TILE_TITLE_ID); + snprintf(sce_sys_dir, sizeof sce_sys_dir, "/user/app/%s/sce_sys", TILE_TITLE_ID); + snprintf(param_path, sizeof param_path, "/user/app/%s/sce_sys/param.json", TILE_TITLE_ID); + snprintf(icon_path, sizeof icon_path, "/user/app/%s/sce_sys/icon0.png", TILE_TITLE_ID); + + /* stat-guard: if everything on disk already matches, do nothing. Re-running + the install API every payload start would be wasted work and burns the + only safe path through Sony's installer state machine. */ + { + struct stat st; + if (stat(base_dir, &st) == 0 && + file_matches(param_path, tile_param_json, tile_param_json_size) && + file_matches(icon_path, tile_icon0_png, tile_icon0_png_size)) { + snprintf(msg, msg_sz, "tile already installed and up to date"); + return 0; + } + } + + /* AppInstUtil is loaded lazily by the install backend thread. Poke it + + poll briefly so libSceAppInstUtil.sprx is mapped before we try to + resolve symbols out of it. Bounded to a few seconds so a stuck init + doesn't wedge an MHD worker forever. */ + { + char ready_msg[128]; + int ready = -1; + for (int i = 0; i < 60 && ready != 0; i++) { + ready = patchdl_install_backend_check(ready_msg, sizeof ready_msg); + if (ready != 0) usleep(250 * 1000); + } + if (ready != 0) { + snprintf(msg, msg_sz, + "install backend not ready: %s", ready_msg); + return -1; + } + } + + /* Resolve the install API now so we can fail fast before touching disk. + itsPLK uses the NID (Wudg3Xe3heE) because the symbol export is + Sony-private; try both, NID first since it survives a stripped sprx. */ + ai_install_title = (ai_install_dir_fn)dynsym_by_nid("Wudg3Xe3heE"); + if (!ai_install_title) + ai_install_title = (ai_install_dir_fn)dynsym_by_name( + "sceAppInstUtilAppInstallTitleDir"); + if (!ai_install_title) { + snprintf(msg, msg_sz, "sceAppInstUtilAppInstallTitleDir not resolved"); + return -1; + } + + ai_initialize = (ai_init_fn)dynsym_by_name("sceAppInstUtilInitialize"); + if (ai_initialize) { + rc = ai_initialize(); + /* SCE_OK == 0; a non-zero rc here usually means "already initialised" + in this process, which is fine. We only bail on a clearly fatal + code (anything that isn't already the success case). */ + if (rc != 0 && rc != 0x80B21161 /* ALREADY_INITIALIZED */) { + snprintf(msg, msg_sz, + "sceAppInstUtilInitialize failed 0x%08x", (unsigned)rc); + return -1; + } + } + + if (mkdir(base_dir, 0755) && errno != EEXIST) { + snprintf(msg, msg_sz, "mkdir %s failed errno=%d", base_dir, errno); + return -1; + } + if (mkdir(sce_sys_dir, 0755) && errno != EEXIST) { + snprintf(msg, msg_sz, "mkdir %s failed errno=%d", sce_sys_dir, errno); + return -1; + } + + if (write_all(param_path, tile_param_json, tile_param_json_size)) { + snprintf(msg, msg_sz, "write param.json failed errno=%d", errno); + return -1; + } + if (write_all(icon_path, tile_icon0_png, tile_icon0_png_size)) { + snprintf(msg, msg_sz, "write icon0.png failed errno=%d", errno); + return -1; + } + + rc = ai_install_title(TILE_TITLE_ID, "/user/app/", NULL); + if (rc != 0) { + snprintf(msg, msg_sz, + "AppInstallTitleDir(%s) returned 0x%08x", + TILE_TITLE_ID, (unsigned)rc); + return -1; + } + + snprintf(msg, msg_sz, "tile installed (%s)", TILE_TITLE_ID); + return 0; +} diff --git a/src/patchdl_tile.h b/src/patchdl_tile.h new file mode 100644 index 0000000..5335ab4 --- /dev/null +++ b/src/patchdl_tile.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +/* Install / refresh the PatchDL home-screen tile. + * + * Approach (from itsPLK's ps5-payload-manager/app_installer.c, which adapted + * John Tornblom's ftpsrv work): write param.json + icon0.png into + * /user/app//sce_sys/ + * then call sceAppInstUtilAppInstallTitleDir(title_id, "/user/app/", 0) + * which registers the directory as an app. The tile's deeplinkUri opens + * Sony's WebKit browser at http://127.0.0.1:12880/, i.e. PatchDL's own UI + * — only useful while the ELF is running. + * + * No PKG, no code signing, no debug-magic. Files only — Sony's installer + * registers the directory as an app. + * + * stat-guard: the asset bytes are diffed against the on-disk copy first; + * if nothing changed the install API isn't called at all (avoids a costly + * re-register every payload start, and avoids the dangerous CancelInstall + * code path). Returns 0 on success or when nothing needed doing. + */ +int patchdl_tile_install_if_needed(char *msg, size_t msg_sz); diff --git a/src/patchdl_websrv.c b/src/patchdl_websrv.c index 18b6855..05ea929 100644 --- a/src/patchdl_websrv.c +++ b/src/patchdl_websrv.c @@ -5,6 +5,7 @@ #include "patchdl_install.h" #include "patchdl_net.h" #include "patchdl_resolve.h" +#include "patchdl_tile.h" #include "patchdl_verxml.h" #include "patchdl_version.h" @@ -1847,6 +1848,7 @@ static enum MHD_Result handle_config_post(struct MHD_Connection *conn, const char *body) { char pol[8]; int mc; + int tile_just_enabled = 0; json_get_str(body, "default_policy", pol, sizeof(pol)); /* Only the two real values are accepted; anything else is silently @@ -1866,8 +1868,12 @@ handle_config_post(struct MHD_Connection *conn, const char *body) { json_get_bool(body, "delete_pkg_after_install", g_cfg.delete_pkg_after_install); g_cfg.verify_downloads = json_get_bool(body, "verify_downloads", g_cfg.verify_downloads); - g_cfg.home_shortcut = - json_get_bool(body, "home_shortcut", g_cfg.home_shortcut); + { + int prev_tile = g_cfg.home_shortcut; + g_cfg.home_shortcut = + json_get_bool(body, "home_shortcut", g_cfg.home_shortcut); + tile_just_enabled = (!prev_tile && g_cfg.home_shortcut); + } g_cfg.max_connections = json_get_int(body, "max_connections", g_cfg.max_connections); if (g_cfg.max_connections < 1) g_cfg.max_connections = 1; @@ -1887,6 +1893,16 @@ handle_config_post(struct MHD_Connection *conn, const char *body) { pthread_mutex_unlock(&g_pool.mtx); save_config(); + + /* Trigger the tile install when the toggle just flipped on. Best-effort — + the config save is already committed; we don't want a tile error to + roll that back. Stat-guard inside the helper makes repeated calls a + no-op, so a save without flipping the toggle still costs nothing. */ + if (tile_just_enabled) { + char tile_msg[256]; + (void)patchdl_tile_install_if_needed(tile_msg, sizeof(tile_msg)); + } + return queue_json_owned(conn, MHD_HTTP_OK, build_config_json()); } @@ -1998,6 +2014,15 @@ on_request(void *cls, struct MHD_Connection *conn, const char *url, return queue_json_owned(conn, rc == 0 ? MHD_HTTP_OK : MHD_HTTP_BAD_GATEWAY, strdup(resp)); } + if (!strcmp(url, "/api/install_tile")) { + char msg[256], resp[320]; + int rc = patchdl_tile_install_if_needed(msg, sizeof(msg)); + snprintf(resp, sizeof(resp), + "{\"ok\":%s,\"rc\":%d,\"message\":\"%s\"}", + rc == 0 ? "true" : "false", rc, msg); + return queue_json_owned(conn, rc == 0 ? MHD_HTTP_OK : MHD_HTTP_BAD_GATEWAY, + strdup(resp)); + } return queue_text(conn, MHD_HTTP_NOT_FOUND, "not found"); } @@ -2251,6 +2276,15 @@ patchdl_websrv_start(unsigned short port) { g_titles[i].enabled = (g_titles[i].source_type != PATCHDL_SOURCE_UNKNOWN); load_config(); + /* If the user opted into a home-screen tile, refresh it now. The helper + is stat-guarded — when nothing changed on disk it's a no-op, so the + cost on subsequent startups is two file reads. Best-effort: never + block startup on a tile install failure. */ + if (g_cfg.home_shortcut) { + char tile_msg[256]; + (void)patchdl_tile_install_if_needed(tile_msg, sizeof(tile_msg)); + } + /* Flag titles that have a partial download on disk so the UI can offer Resume after a reboot. */ detect_resumable_partials();