mirror of
https://github.com/knutwurst/patchdl.git
synced 2026-10-06 09:00:23 +02:00
Add read-only pkg diagnostics: manifest dump, integrity verify, embedded ids
Three read-only endpoints (no install, no writes) to inspect a downloaded package on-device: - GET /api/manifest/<title_id> — re-fetch the patch manifest (PatchDL bypasses the DNS block) and dump each piece's offset/size/SHA-256. - GET /api/pkgverify/<title_id> — SHA-256 every piece of the assembled .pkg against the manifest hashes, on-device (SSD, no multi-GB transfer), and report per-piece pass/fail. Proves whether the file is byte-correct. - GET /api/pkgmeta/<title_id> — read the pkg's embedded content id + title id (GetContentIdFromPkg) vs the target ids, to expose cross-region linkage. Supporting code: patchdl_sha256_fd_region() (pread + OpenSSL EVP) in the net layer, and bind sceAppInstUtilGetContentIdFromPkg in the install backend. Used to diagnose the Dead Island 2 install: the 61.6 GB package verifies byte-perfect (17/17 pieces) and its content id matches the target, so the 0x80B2116F install rejection is a Sony install-method limitation, not the data.
This commit is contained in:
1 parent
986ff00c36
commit
3d2ee430e1
5 files changed
+211
-4
No files matched your search
+58
-4
@@ -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,
|
typedef int (*ai_install_by_pkg_fn)(ai_meta_info_t *meta, ai_pkg_info_t *info,
|
||||||
ai_playgo_info_t *playgo);
|
ai_playgo_info_t *playgo);
|
||||||
typedef int (*ai_title_from_pkg_fn)(const char *path, char *title_id, int *is_app);
|
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_init_fn ai_initialize;
|
||||||
static ai_install_pkg_fn ai_install_pkg;
|
static ai_install_pkg_fn ai_install_pkg;
|
||||||
static ai_install_by_pkg_fn ai_install_by_package;
|
static ai_install_by_pkg_fn ai_install_by_package;
|
||||||
static ai_title_from_pkg_fn ai_title_from_pkg;
|
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
|
/* Resolve + initialize the AppInstUtil backend WITHOUT linking the sce libs
|
||||||
(that makes the ELF unloadable by the elfldr) and WITHOUT raw
|
(that makes the ELF unloadable by the elfldr) and WITHOUT raw
|
||||||
@@ -205,6 +207,8 @@ backend_init_thread(void *arg) {
|
|||||||
"sceAppInstUtilInstallByPackage");
|
"sceAppInstUtilInstallByPackage");
|
||||||
ai_title_from_pkg = (ai_title_from_pkg_fn)dynsym("libSceAppInstUtil.sprx",
|
ai_title_from_pkg = (ai_title_from_pkg_fn)dynsym("libSceAppInstUtil.sprx",
|
||||||
"sceAppInstUtilGetTitleIdFromPkg");
|
"sceAppInstUtilGetTitleIdFromPkg");
|
||||||
|
ai_content_from_pkg = (ai_content_from_pkg_fn)dynsym("libSceAppInstUtil.sprx",
|
||||||
|
"sceAppInstUtilGetContentIdFromPkg");
|
||||||
|
|
||||||
/* Read-only feasibility probe — module is loaded, safe context. */
|
/* Read-only feasibility probe — module is loaded, safe context. */
|
||||||
fill_probe();
|
fill_probe();
|
||||||
@@ -284,6 +288,56 @@ patchdl_install_api_probe(char *out, size_t out_sz) {
|
|||||||
return -1;
|
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
|
int
|
||||||
patchdl_install_local_pkg(const char *local_path, const char *expected_title_id,
|
patchdl_install_local_pkg(const char *local_path, const char *expected_title_id,
|
||||||
const char *storage_title_id,
|
const char *storage_title_id,
|
||||||
|
|||||||
@@ -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
|
AppInstUtil/Bgft patch-install symbols and report which exist on this
|
||||||
firmware. Writes a JSON object into `out`. No install, no side effects. */
|
firmware. Writes a JSON object into `out`. No install, no side effects. */
|
||||||
int patchdl_install_api_probe(char *out, size_t out_sz);
|
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);
|
||||||
@@ -807,6 +807,41 @@ patchdl_http_download_piece(const char *url, int fd,
|
|||||||
return 0;
|
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
|
void
|
||||||
patchdl_manifest_free(patchdl_manifest_t *m) {
|
patchdl_manifest_free(patchdl_manifest_t *m) {
|
||||||
if (!m || !m->pieces) return;
|
if (!m || !m->pieces) return;
|
||||||
|
|||||||
@@ -57,6 +57,11 @@ int patchdl_http_download_piece(const char *url, int fd,
|
|||||||
const char *expected_sha256_or_null,
|
const char *expected_sha256_or_null,
|
||||||
patchdl_piece_ctx_t *ctx);
|
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
|
/* Progress callback. Return non-zero to ABORT the in-flight download (used to
|
||||||
cancel large patch downloads); return 0 to continue. */
|
cancel large patch downloads); return 0 to continue. */
|
||||||
typedef int (*patchdl_download_progress_cb)(void *ctx,
|
typedef int (*patchdl_download_progress_cb)(void *ctx,
|
||||||
|
|||||||
@@ -1807,6 +1807,113 @@ on_request(void *cls, struct MHD_Connection *conn, const char *url,
|
|||||||
if (!strcmp(url, "/api/pkgdiag"))
|
if (!strcmp(url, "/api/pkgdiag"))
|
||||||
return queue_json(conn, MHD_HTTP_OK, g_pkg_diag_json);
|
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")) {
|
if (!strcmp(url, "/api/netcheck")) {
|
||||||
char diag[1024] = "{\"error\":\"no title with version_file_uri\"}";
|
char diag[1024] = "{\"error\":\"no title with version_file_uri\"}";
|
||||||
pthread_mutex_lock(&g_mutex);
|
pthread_mutex_lock(&g_mutex);
|
||||||
|
|||||||
Reference in new issue
Block a user