diff --git a/src/patchdl_install.c b/src/patchdl_install.c index 093d15c..0cbfc64 100644 --- a/src/patchdl_install.c +++ b/src/patchdl_install.c @@ -57,11 +57,13 @@ typedef int (*ai_install_pkg_fn)(const char *path, ai_pkg_info_t *info); typedef int (*ai_install_by_pkg_fn)(ai_meta_info_t *meta, ai_pkg_info_t *info, ai_playgo_info_t *playgo); typedef int (*ai_title_from_pkg_fn)(const char *path, char *title_id, int *is_app); +typedef int (*ai_content_from_pkg_fn)(const char *path, char *content_id, int *is_app); -static ai_init_fn ai_initialize; -static ai_install_pkg_fn ai_install_pkg; -static ai_install_by_pkg_fn ai_install_by_package; -static ai_title_from_pkg_fn ai_title_from_pkg; +static ai_init_fn ai_initialize; +static ai_install_pkg_fn ai_install_pkg; +static ai_install_by_pkg_fn ai_install_by_package; +static ai_title_from_pkg_fn ai_title_from_pkg; +static ai_content_from_pkg_fn ai_content_from_pkg; /* Resolve + initialize the AppInstUtil backend WITHOUT linking the sce libs (that makes the ELF unloadable by the elfldr) and WITHOUT raw @@ -205,6 +207,8 @@ backend_init_thread(void *arg) { "sceAppInstUtilInstallByPackage"); ai_title_from_pkg = (ai_title_from_pkg_fn)dynsym("libSceAppInstUtil.sprx", "sceAppInstUtilGetTitleIdFromPkg"); + ai_content_from_pkg = (ai_content_from_pkg_fn)dynsym("libSceAppInstUtil.sprx", + "sceAppInstUtilGetContentIdFromPkg"); /* Read-only feasibility probe — module is loaded, safe context. */ fill_probe(); @@ -284,6 +288,56 @@ patchdl_install_api_probe(char *out, size_t out_sz) { return -1; } +/* Read-only: report the .pkg's embedded content id + title id (and whether it + is a full app vs a patch). No install, no side effects. 0 if anything read. */ +int +patchdl_install_pkg_meta(const char *local_path, char *content_id, size_t cid_sz, + char *title_id, size_t tid_sz, int *is_app, + char *msg, size_t msg_sz) { + char sdk_path[1024]; + char cid[64] = {0}, tid[48] = {0}; + int app_c = 0, app_t = 0, ok = 0; + struct stat st; + + if (content_id && cid_sz) content_id[0] = '\0'; + if (title_id && tid_sz) title_id[0] = '\0'; + if (is_app) *is_app = 0; + + if (!local_path || !local_path[0] || stat(local_path, &st) != 0) { + snprintf(msg, msg_sz, "package not on disk"); + return -1; + } + backend_start(); + if (g_stage != 5) { + snprintf(msg, msg_sz, "install backend not ready: %s", stage_str(g_stage)); + return -1; + } + if (!strncmp(local_path, "/data/", 6)) + snprintf(sdk_path, sizeof sdk_path, "/user%s", local_path); + else + snprintf(sdk_path, sizeof sdk_path, "%s", local_path); + + if (ai_content_from_pkg && + ai_content_from_pkg(sdk_path, cid, &app_c) == 0 && cid[0]) { + if (content_id && cid_sz) { + strncpy(content_id, cid, cid_sz - 1); + content_id[cid_sz - 1] = '\0'; + } + if (is_app) *is_app = app_c; + ok = 1; + } + if (ai_title_from_pkg && + ai_title_from_pkg(sdk_path, tid, &app_t) == 0 && tid[0]) { + if (title_id && tid_sz) { + strncpy(title_id, tid, tid_sz - 1); + title_id[tid_sz - 1] = '\0'; + } + ok = 1; + } + snprintf(msg, msg_sz, ok ? "ok" : "could not read pkg metadata"); + return ok ? 0 : -1; +} + int patchdl_install_local_pkg(const char *local_path, const char *expected_title_id, const char *storage_title_id, diff --git a/src/patchdl_install.h b/src/patchdl_install.h index bfb12af..cba34e8 100644 --- a/src/patchdl_install.h +++ b/src/patchdl_install.h @@ -31,3 +31,9 @@ int patchdl_install_backend_check(char *msg, size_t msg_sz); AppInstUtil/Bgft patch-install symbols and report which exist on this firmware. Writes a JSON object into `out`. No install, no side effects. */ int patchdl_install_api_probe(char *out, size_t out_sz); + +/* Read-only: report the .pkg's embedded content id + title id (and whether it + is a full app vs a patch, via *is_app). No install. 0 if anything was read. */ +int patchdl_install_pkg_meta(const char *local_path, char *content_id, size_t cid_sz, + char *title_id, size_t tid_sz, int *is_app, + char *msg, size_t msg_sz); diff --git a/src/patchdl_net.c b/src/patchdl_net.c index 37e4d7a..ccbe4e9 100644 --- a/src/patchdl_net.c +++ b/src/patchdl_net.c @@ -807,6 +807,41 @@ patchdl_http_download_piece(const char *url, int fd, return 0; } +/* Read-only: SHA-256 a [offset, offset+size) region of fd into out_hex (>=65 + bytes). Uses pread so it doesn't disturb the fd offset. 0 on success. */ +int +patchdl_sha256_fd_region(int fd, long long offset, long long size, char *out_hex) { + EVP_MD_CTX *md; + unsigned char *buf; + long long pos = offset, remaining = size; + const size_t CHUNK = 1u << 20; + + out_hex[0] = '\0'; + if (fd < 0 || size < 0) return -1; + md = EVP_MD_CTX_new(); + if (!md) return -1; + buf = malloc(CHUNK); + if (!buf) { EVP_MD_CTX_free(md); return -1; } + EVP_DigestInit_ex(md, EVP_sha256(), NULL); + while (remaining > 0) { + size_t want = remaining > (long long)CHUNK ? CHUNK : (size_t)remaining; + ssize_t got = pread(fd, buf, want, (off_t)pos); + if (got <= 0) { free(buf); EVP_MD_CTX_free(md); return -1; } + EVP_DigestUpdate(md, buf, (size_t)got); + pos += got; remaining -= got; + } + { + unsigned char dig[EVP_MAX_MD_SIZE]; + unsigned int dl = 0, i; + EVP_DigestFinal_ex(md, dig, &dl); + for (i = 0; i < dl; i++) sprintf(out_hex + 2 * i, "%02x", dig[i]); + out_hex[2 * dl] = '\0'; + } + free(buf); + EVP_MD_CTX_free(md); + return 0; +} + void patchdl_manifest_free(patchdl_manifest_t *m) { if (!m || !m->pieces) return; diff --git a/src/patchdl_net.h b/src/patchdl_net.h index fda4f23..9540721 100644 --- a/src/patchdl_net.h +++ b/src/patchdl_net.h @@ -57,6 +57,11 @@ int patchdl_http_download_piece(const char *url, int fd, const char *expected_sha256_or_null, patchdl_piece_ctx_t *ctx); +/* Read-only: SHA-256 a [offset, offset+size) region of fd into out_hex + (caller provides >= 65 bytes). Returns 0 on success. */ +int patchdl_sha256_fd_region(int fd, long long offset, long long size, + char *out_hex); + /* Progress callback. Return non-zero to ABORT the in-flight download (used to cancel large patch downloads); return 0 to continue. */ typedef int (*patchdl_download_progress_cb)(void *ctx, diff --git a/src/patchdl_websrv.c b/src/patchdl_websrv.c index 52196a7..ef1d39f 100644 --- a/src/patchdl_websrv.c +++ b/src/patchdl_websrv.c @@ -1807,6 +1807,113 @@ on_request(void *cls, struct MHD_Connection *conn, const char *url, if (!strcmp(url, "/api/pkgdiag")) return queue_json(conn, MHD_HTTP_OK, g_pkg_diag_json); + /* Read-only diagnostic: re-fetch the patch manifest for a title (PatchDL can + bypass the DNS block) and dump each piece's offset/size/SHA-256 so the + assembled .pkg can be verified against Sony's own hashes. No install. */ + if (!strncmp(url, "/api/manifest/", 14)) { + const char *tid = url + 14; + char purl[768] = {0}, pti[32], psti[32], cid[64], nm[128], ver[16]; + patchdl_source_t src; + int en; + patchdl_manifest_t mf; + jbuf_t j = {0}; + + if (!get_title_action_info(tid, &src, purl, sizeof purl, pti, sizeof pti, + psti, sizeof psti, cid, sizeof cid, + nm, sizeof nm, ver, sizeof ver, &en) || !purl[0]) + return queue_json(conn, MHD_HTTP_NOT_FOUND, + "{\"error\":\"no patch_url for title\"}"); + memset(&mf, 0, sizeof mf); + if (patchdl_fetch_manifest(purl, &mf) != 0) + return queue_json(conn, MHD_HTTP_BAD_GATEWAY, + "{\"error\":\"manifest fetch failed\"}"); + jbuf_appendf(&j, "{\"count\":%d,\"total\":%lld,\"pieces\":[", + mf.count, mf.total); + for (int i = 0; i < mf.count; i++) { + if (i) jbuf_append(&j, ","); + jbuf_appendf(&j, "{\"o\":%lld,\"s\":%lld,\"h\":\"%s\"}", + mf.pieces[i].offset, mf.pieces[i].size, mf.pieces[i].hash); + } + jbuf_append(&j, "]}"); + patchdl_manifest_free(&mf); + return queue_json_owned(conn, MHD_HTTP_OK, + j.buf ? j.buf : strdup("{}")); + } + + /* Read-only: SHA-256 every piece of the on-disk .pkg and compare to Sony's + manifest hashes — proves whether the assembled file is byte-correct. No + install. Hashing runs on-device (SSD), so no multi-GB transfer. */ + if (!strncmp(url, "/api/pkgverify/", 15)) { + const char *tid = url + 15; + char purl[768] = {0}, pti[32], psti[32], cid[64], nm[128], ver[16]; + char dest[320]; + patchdl_source_t src; + int en, fd, okc = 0, badc = 0, first_bad = -1; + patchdl_manifest_t mf; + jbuf_t j = {0}; + + if (!get_title_action_info(tid, &src, purl, sizeof purl, pti, sizeof pti, + psti, sizeof psti, cid, sizeof cid, + nm, sizeof nm, ver, sizeof ver, &en) || !purl[0]) + return queue_json(conn, MHD_HTTP_NOT_FOUND, + "{\"error\":\"no patch_url for title\"}"); + title_pkg_path(tid, purl, dest, sizeof dest); + fd = open(dest, O_RDONLY); + if (fd < 0) + return queue_json(conn, MHD_HTTP_NOT_FOUND, + "{\"error\":\"pkg not on disk\"}"); + memset(&mf, 0, sizeof mf); + if (patchdl_fetch_manifest(purl, &mf) != 0) { + close(fd); + return queue_json(conn, MHD_HTTP_BAD_GATEWAY, + "{\"error\":\"manifest fetch failed\"}"); + } + jbuf_appendf(&j, "{\"count\":%d,\"pieces\":[", mf.count); + for (int i = 0; i < mf.count; i++) { + char got[80] = {0}; + int ok = 0; + if (mf.pieces[i].hash[0] && + patchdl_sha256_fd_region(fd, mf.pieces[i].offset, + mf.pieces[i].size, got) == 0) + ok = (strcasecmp(got, mf.pieces[i].hash) == 0); + if (ok) okc++; else { badc++; if (first_bad < 0) first_bad = i; } + if (i) jbuf_append(&j, ","); + jbuf_appendf(&j, "{\"i\":%d,\"o\":%lld,\"s\":%lld,\"ok\":%s}", + i, mf.pieces[i].offset, mf.pieces[i].size, + ok ? "true" : "false"); + } + jbuf_appendf(&j, "],\"ok\":%d,\"bad\":%d,\"first_bad\":%d,\"all_ok\":%s}", + okc, badc, first_bad, (badc == 0 ? "true" : "false")); + patchdl_manifest_free(&mf); + close(fd); + return queue_json_owned(conn, MHD_HTTP_OK, j.buf ? j.buf : strdup("{}")); + } + + /* Read-only: report the .pkg's embedded content id / title id vs the target + (installed app) ids, to expose the cross-region linkage. No install. */ + if (!strncmp(url, "/api/pkgmeta/", 13)) { + const char *tid = url + 13; + char purl[768] = {0}, pti[32], psti[32], cid[64], nm[128], ver[16]; + char dest[320], ecid[64] = {0}, etid[48] = {0}, msg[128], resp[900]; + patchdl_source_t src; + int en, is_app = 0, rc; + + if (!get_title_action_info(tid, &src, purl, sizeof purl, pti, sizeof pti, + psti, sizeof psti, cid, sizeof cid, + nm, sizeof nm, ver, sizeof ver, &en) || !purl[0]) + return queue_json(conn, MHD_HTTP_NOT_FOUND, "{\"error\":\"unknown title\"}"); + title_pkg_path(tid, purl, dest, sizeof dest); + rc = patchdl_install_pkg_meta(dest, ecid, sizeof ecid, etid, sizeof etid, + &is_app, msg, sizeof msg); + snprintf(resp, sizeof resp, + "{\"ok\":%s,\"pkg_content_id\":\"%s\",\"pkg_title_id\":\"%s\"," + "\"is_app\":%s,\"target_content_id\":\"%s\",\"target_title_id\":\"%s\"," + "\"storage_title_id\":\"%s\",\"msg\":\"%s\"}", + rc == 0 ? "true" : "false", ecid, etid, is_app ? "true" : "false", + cid, tid, psti, msg); + return queue_json_owned(conn, MHD_HTTP_OK, strdup(resp)); + } + if (!strcmp(url, "/api/netcheck")) { char diag[1024] = "{\"error\":\"no title with version_file_uri\"}"; pthread_mutex_lock(&g_mutex);