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,
|
||||
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,
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user