mirror of
https://github.com/knutwurst/patchdl.git
synced 2026-10-06 10:00:22 +02:00
Sony path-allowlists the URI given to sceAppInstUtilInstallByPackage — /user/data/ and /mnt/usb are accepted, a bare /data/... path is rejected with 0x80B2116F (confirmed by the ps5upload project). PatchDL stored the pkg under /data/patchdl and passed that /data path, so every install was rejected at the path stage. Pass the /user/data view of the same file instead (the code already computed it as sdk_path; it was only used for AppInstallPkg before). Also report each URI's individual rc instead of only the last attempt's, which revealed the real wall: via file:// the installer reaches header parsing and rejects with 0x80B21106 — the assembled file is a valid but DEBUG-magic PKG (\x7FFIH, not retail \x7FCNT), the format Sony's system updater consumes rather than the retail-pkg format InstallByPackage expects.
492 lines
18 KiB
C
492 lines
18 KiB
C
#include "patchdl_install.h"
|
|
|
|
#include <arpa/inet.h>
|
|
#include <ifaddrs.h>
|
|
#include <netinet/in.h>
|
|
#include <ps5/kernel.h>
|
|
|
|
#include <pthread.h>
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/stat.h>
|
|
|
|
/* AppInstUtil structs/signatures, reverse-engineered by the PS5 homebrew
|
|
scene (notmaj0r/CheatRunner, ps5-payload-dev/websrv). Sizes per CheatRunner:
|
|
CONTENTID=0x30, PLAYGOSCENARIOID=3, LANGUAGE=8, NUM_LANGUAGES=30, NUM_IDS=64.
|
|
The trailing `unknown` padding makes the playgo struct safely large. */
|
|
#define AI_CONTENTID_SIZE 0x30
|
|
#define AI_PLAYGOSCENARIOID_SIZE 3
|
|
#define AI_LANGUAGE_SIZE 8
|
|
#define AI_NUM_LANGUAGES 30
|
|
#define AI_NUM_IDS 64
|
|
|
|
typedef struct {
|
|
char content_id[AI_CONTENTID_SIZE];
|
|
int content_type;
|
|
int content_platform;
|
|
} ai_pkg_info_t;
|
|
|
|
typedef struct {
|
|
const char *uri;
|
|
const char *ex_uri;
|
|
const char *playgo_scenario_id;
|
|
const char *content_id;
|
|
const char *content_name;
|
|
const char *icon_url;
|
|
} ai_meta_info_t;
|
|
|
|
typedef struct {
|
|
char languages[AI_NUM_LANGUAGES][AI_LANGUAGE_SIZE];
|
|
char playgo_scenario_ids[AI_NUM_IDS][AI_PLAYGOSCENARIOID_SIZE];
|
|
char content_ids[AI_NUM_IDS][AI_CONTENTID_SIZE];
|
|
long unknown[810];
|
|
} ai_playgo_info_t;
|
|
|
|
/* Sysmodule IDs (from ps5-payload-dev/sdk crt/rtld_sprx.c). */
|
|
#define SYSMOD_IPMI 0x8000001d
|
|
#define SYSMOD_USERSERVICE 0x80000011
|
|
#define SYSMOD_SYSTEMSERVICE 0x80000010
|
|
#define SYSMOD_APPINSTUTIL 0x80000014
|
|
|
|
typedef int (*sysmod_load_fn)(unsigned int id);
|
|
typedef int (*ai_init_fn)(void);
|
|
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_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
|
|
sceKernelLoadStartModule (that wedged the process). Instead use the proper
|
|
sysmodule loader — the same path the SDK's rtld_sprx takes — then resolve
|
|
symbols via the kernel dynlib helpers. Runs in a detached thread; the HTTP
|
|
handler reports the stage and never blocks.
|
|
stage: 0 idle, 1 resolve loader, 2 load modules, 3 resolve symbols,
|
|
4 initialize, 5 ready, negative = failure at that step. */
|
|
static volatile int g_stage;
|
|
static int g_err;
|
|
static pthread_mutex_t g_mtx = PTHREAD_MUTEX_INITIALIZER;
|
|
static char g_probe_json[2048]; /* filled by the backend thread */
|
|
|
|
static intptr_t
|
|
dynsym(const char *module, const char *sym) {
|
|
uint32_t h = 0;
|
|
if (kernel_dynlib_handle(-1, module, &h) < 0)
|
|
return 0;
|
|
return kernel_dynlib_dlsym(-1, h, sym);
|
|
}
|
|
|
|
static void
|
|
local_ip(char *out, size_t n) {
|
|
struct ifaddrs *ifa = NULL, *p;
|
|
|
|
out[0] = '\0';
|
|
if (getifaddrs(&ifa))
|
|
return;
|
|
|
|
for (p = ifa; p; p = p->ifa_next) {
|
|
char ip[INET_ADDRSTRLEN];
|
|
struct sockaddr_in *s;
|
|
|
|
if (!p->ifa_addr || p->ifa_addr->sa_family != AF_INET)
|
|
continue;
|
|
s = (struct sockaddr_in *)p->ifa_addr;
|
|
if (!inet_ntop(AF_INET, &s->sin_addr, ip, sizeof(ip)))
|
|
continue;
|
|
if (strcmp(ip, "127.0.0.1") && strncmp(ip, "0.", 2)) {
|
|
strncpy(out, ip, n - 1);
|
|
out[n - 1] = '\0';
|
|
break;
|
|
}
|
|
}
|
|
freeifaddrs(ifa);
|
|
}
|
|
|
|
/* Resolve (dlsym, never call) a list of candidate patch-install symbols and
|
|
record which exist. Runs inside the backend thread, where the AppInstUtil
|
|
module is already loaded — the same proven-safe context as the normal symbol
|
|
resolution. No sysmod_load and no calls, so it is side-effect free. */
|
|
static void
|
|
fill_probe(void) {
|
|
static const char *ai_syms[] = {
|
|
"sceAppInstUtilInitialize",
|
|
"sceAppInstUtilAppInstallPkg",
|
|
"sceAppInstUtilAppInstallTitleDir", /* takes an explicit title id */
|
|
"sceAppInstUtilInstallByPackage",
|
|
"sceAppInstUtilInstallByPackageEx",
|
|
"sceAppInstUtilGetTitleIdFromPkg",
|
|
"sceAppInstUtilGetContentIdFromPkg",
|
|
"sceAppInstUtilAppExist",
|
|
"sceAppInstUtilAppGetInstallStatus",
|
|
"sceAppInstUtilAppInstallStatus",
|
|
"sceAppInstUtilAppUnInstall",
|
|
"sceAppInstUtilGetMetaInfoFromPkg",
|
|
"sceAppInstUtilUpdateTitleByTitleId",
|
|
"sceAppInstUtilInstallByChunk",
|
|
NULL
|
|
};
|
|
static const char *bgft_syms[] = {
|
|
"sceBgftInitialize",
|
|
"sceBgftServiceIntInit",
|
|
"sceBgftServiceDownloadRegisterTask",
|
|
"sceBgftServiceDownloadRegisterTaskByStorage",
|
|
"sceBgftServiceDownloadRegisterTaskByStorageEx",
|
|
"sceBgftServiceIntDownloadRegisterTaskByStorageEx",
|
|
"sceBgftServiceDownloadStartTask",
|
|
"sceBgftServiceDownloadGetProgress",
|
|
"sceBgftServiceInstallPackage",
|
|
NULL
|
|
};
|
|
uint32_t h = 0;
|
|
int ai_loaded = (kernel_dynlib_handle(-1, "libSceAppInstUtil.sprx", &h) >= 0);
|
|
int bgft_loaded = (kernel_dynlib_handle(-1, "libSceBgft.sprx", &h) >= 0);
|
|
char tmp[sizeof(g_probe_json)];
|
|
size_t n = 0, sz = sizeof(tmp);
|
|
char *out = tmp;
|
|
int first = 1;
|
|
|
|
n += snprintf(out + n, sz - n,
|
|
"{\"appinstutil_loaded\":%s,\"bgft_loaded\":%s,\"symbols\":{",
|
|
ai_loaded ? "true" : "false", bgft_loaded ? "true" : "false");
|
|
for (int i = 0; ai_syms[i] && n < sz - 80; i++) {
|
|
intptr_t a = dynsym("libSceAppInstUtil.sprx", ai_syms[i]);
|
|
n += snprintf(out + n, sz - n, "%s\"%s\":%s",
|
|
first ? "" : ",", ai_syms[i], a ? "true" : "false");
|
|
first = 0;
|
|
}
|
|
for (int i = 0; bgft_syms[i] && n < sz - 80; i++) {
|
|
intptr_t a = bgft_loaded ? dynsym("libSceBgft.sprx", bgft_syms[i]) : 0;
|
|
n += snprintf(out + n, sz - n, "%s\"%s\":%s",
|
|
first ? "" : ",", bgft_syms[i], a ? "true" : "false");
|
|
first = 0;
|
|
}
|
|
snprintf(out + n, sz - n, "}}");
|
|
|
|
/* Publish atomically: the getter runs on an MHD worker thread and reads
|
|
g_probe_json under the same lock, so it never sees a half-built buffer. */
|
|
pthread_mutex_lock(&g_mtx);
|
|
memcpy(g_probe_json, tmp, sizeof(g_probe_json));
|
|
pthread_mutex_unlock(&g_mtx);
|
|
}
|
|
|
|
static void *
|
|
backend_init_thread(void *arg) {
|
|
(void)arg;
|
|
sysmod_load_fn sysmod_load;
|
|
|
|
g_stage = 1;
|
|
sysmod_load = (sysmod_load_fn)dynsym("libSceSysmodule.sprx",
|
|
"sceSysmoduleLoadModuleInternal");
|
|
if (!sysmod_load) { g_stage = -1; return NULL; }
|
|
|
|
g_stage = 2;
|
|
/* Dependencies first, then AppInstUtil. The sysmodule loader pulls in
|
|
further deps and is the non-wedging path. */
|
|
sysmod_load(SYSMOD_IPMI);
|
|
sysmod_load(SYSMOD_USERSERVICE);
|
|
sysmod_load(SYSMOD_SYSTEMSERVICE);
|
|
g_err = sysmod_load(SYSMOD_APPINSTUTIL);
|
|
if (g_err) { g_stage = -2; return NULL; }
|
|
|
|
g_stage = 3;
|
|
ai_initialize = (ai_init_fn)dynsym("libSceAppInstUtil.sprx",
|
|
"sceAppInstUtilInitialize");
|
|
ai_install_pkg = (ai_install_pkg_fn)dynsym("libSceAppInstUtil.sprx",
|
|
"sceAppInstUtilAppInstallPkg");
|
|
ai_install_by_package = (ai_install_by_pkg_fn)dynsym("libSceAppInstUtil.sprx",
|
|
"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();
|
|
|
|
if (!ai_initialize || !ai_install_pkg || !ai_install_by_package) {
|
|
g_stage = -3;
|
|
return NULL;
|
|
}
|
|
|
|
g_stage = 4;
|
|
g_err = ai_initialize();
|
|
if (g_err) { g_stage = -4; return NULL; }
|
|
|
|
g_stage = 5;
|
|
return NULL;
|
|
}
|
|
|
|
static void
|
|
backend_start(void) {
|
|
pthread_mutex_lock(&g_mtx);
|
|
if (g_stage == 0) {
|
|
pthread_t tid;
|
|
pthread_attr_t attr;
|
|
g_stage = 1;
|
|
pthread_attr_init(&attr);
|
|
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
|
|
pthread_create(&tid, &attr, backend_init_thread, NULL);
|
|
pthread_attr_destroy(&attr);
|
|
}
|
|
pthread_mutex_unlock(&g_mtx);
|
|
}
|
|
|
|
static const char *
|
|
stage_str(int s) {
|
|
switch (s) {
|
|
case 0: return "idle";
|
|
case 1: return "resolving sysmodule loader";
|
|
case 2: return "loading AppInstUtil module";
|
|
case 3: return "resolving AppInstUtil symbols";
|
|
case 4: return "initializing service";
|
|
case 5: return "ready";
|
|
case -1: return "sysmodule loader unavailable";
|
|
case -2: return "AppInstUtil module load failed";
|
|
case -3: return "AppInstUtil symbol resolution failed";
|
|
case -4: return "AppInstUtil init failed";
|
|
default: return "unknown";
|
|
}
|
|
}
|
|
|
|
int
|
|
patchdl_install_backend_check(char *msg, size_t msg_sz) {
|
|
int s;
|
|
backend_start();
|
|
s = g_stage;
|
|
snprintf(msg, msg_sz, "%s (rc=0x%08x)", stage_str(s), (unsigned)g_err);
|
|
return (s == 5) ? 0 : -1;
|
|
}
|
|
|
|
/* Public getter: trigger the backend (which fills the probe in its own thread)
|
|
and return the cached result. Runs from the MHD worker thread, so it only
|
|
reads the cached string — it never loads modules or resolves symbols here. */
|
|
int
|
|
patchdl_install_api_probe(char *out, size_t out_sz) {
|
|
int ready;
|
|
|
|
backend_start();
|
|
pthread_mutex_lock(&g_mtx);
|
|
ready = (g_probe_json[0] != '\0');
|
|
if (ready)
|
|
snprintf(out, out_sz, "%s", g_probe_json);
|
|
pthread_mutex_unlock(&g_mtx);
|
|
if (ready)
|
|
return 0;
|
|
|
|
snprintf(out, out_sz,
|
|
"{\"pending\":true,\"stage\":\"%s\"}", stage_str(g_stage));
|
|
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,
|
|
const char *target_content_id,
|
|
char *msg, size_t msg_sz) {
|
|
char sdk_path[1024];
|
|
char pkg_tid[48] = {0};
|
|
struct stat st;
|
|
int rc;
|
|
int pkg_tid_mismatch = 0;
|
|
|
|
if (!local_path || !local_path[0]) {
|
|
snprintf(msg, msg_sz, "no package path");
|
|
return -1;
|
|
}
|
|
if (stat(local_path, &st) != 0) {
|
|
snprintf(msg, msg_sz, "package not downloaded");
|
|
return -1;
|
|
}
|
|
|
|
backend_start();
|
|
if (g_stage != 5) {
|
|
snprintf(msg, msg_sz, "install backend not ready: %s", stage_str(g_stage));
|
|
return -1;
|
|
}
|
|
|
|
/* Sony's installer sees the user partition as /user/data, not /data, and
|
|
PATH-ALLOWLISTS the URI passed to InstallByPackage: /user/data/ and
|
|
/mnt/usb are accepted, but a bare /data/... path is REJECTED with
|
|
0x80B2116F (empirically confirmed by the ps5upload project). So feed the
|
|
/user/data view of the file to both InstallByPackage and AppInstallPkg. */
|
|
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);
|
|
|
|
/* Diagnostic guard: Sony sometimes stores one patch byte stream under a
|
|
master title id while the version.xml targets a regional title id. That
|
|
is valid only when the caller supplies target metadata, so do not feed
|
|
such packages to the raw AppInstallPkg path. */
|
|
if (storage_title_id && storage_title_id[0] &&
|
|
expected_title_id && expected_title_id[0] &&
|
|
strncmp(storage_title_id, expected_title_id, 9) != 0) {
|
|
pkg_tid_mismatch = 1;
|
|
strncpy(pkg_tid, storage_title_id, sizeof(pkg_tid) - 1);
|
|
}
|
|
if (ai_title_from_pkg && expected_title_id && expected_title_id[0]) {
|
|
int is_app = 0;
|
|
if (ai_title_from_pkg(sdk_path, pkg_tid, &is_app) == 0 && pkg_tid[0] &&
|
|
strncmp(pkg_tid, expected_title_id, 9) != 0) {
|
|
pkg_tid_mismatch = 1;
|
|
}
|
|
}
|
|
if (pkg_tid_mismatch && (!target_content_id || !target_content_id[0])) {
|
|
snprintf(msg, msg_sz,
|
|
"refused: package metadata is %.12s, target is %.12s",
|
|
pkg_tid, expected_title_id);
|
|
return -1;
|
|
}
|
|
|
|
/* Preferred path: InstallByPackage accepts target metadata. Use it first,
|
|
and use it exclusively when the downloaded bytes report a master/storage
|
|
title id that differs from the target regional title id. */
|
|
{
|
|
char file_uri[1100];
|
|
char http_loop_uri[1200] = {0};
|
|
char http_lan_uri[1200] = {0};
|
|
const char *uris[4];
|
|
ai_meta_info_t meta = {0};
|
|
ai_pkg_info_t pkg = {0};
|
|
ai_playgo_info_t playgo = {0};
|
|
int rc2 = -1;
|
|
const char *title_dir;
|
|
const char *file_base;
|
|
|
|
snprintf(file_uri, sizeof(file_uri), "file://%s", sdk_path);
|
|
title_dir = strstr(local_path, "/data/patchdl/");
|
|
file_base = strrchr(local_path, '/');
|
|
if (title_dir && file_base && file_base > title_dir + strlen("/data/patchdl/")) {
|
|
char title_id[32] = {0};
|
|
const char *t = title_dir + strlen("/data/patchdl/");
|
|
size_t tlen = (size_t)(file_base - t);
|
|
if (tlen > 0 && tlen < sizeof(title_id)) {
|
|
char ip[INET_ADDRSTRLEN] = {0};
|
|
memcpy(title_id, t, tlen);
|
|
snprintf(http_loop_uri, sizeof(http_loop_uri),
|
|
"http://127.0.0.1:%d/api/pkg/%s/%s",
|
|
PATCHDL_HTTP_PORT, title_id, file_base + 1);
|
|
local_ip(ip, sizeof(ip));
|
|
if (ip[0])
|
|
snprintf(http_lan_uri, sizeof(http_lan_uri),
|
|
"http://%s:%d/api/pkg/%s/%s",
|
|
ip, PATCHDL_HTTP_PORT, title_id, file_base + 1);
|
|
}
|
|
}
|
|
uris[0] = sdk_path; /* /user/data/... — the allowlisted path */
|
|
uris[1] = file_uri; /* file:///user/data/... */
|
|
uris[2] = http_loop_uri[0] ? http_loop_uri : NULL;
|
|
uris[3] = http_lan_uri[0] ? http_lan_uri : NULL;
|
|
meta.ex_uri = "";
|
|
meta.playgo_scenario_id = "";
|
|
meta.content_id = target_content_id ? target_content_id : "";
|
|
meta.content_name = "PatchDL";
|
|
meta.icon_url = "";
|
|
|
|
{
|
|
char tries[260] = {0};
|
|
const char *labels[4] = { "userdata", "file", "loop", "lan" };
|
|
for (int i = 0; i < 4; i++) {
|
|
if (!uris[i]) continue;
|
|
memset(&pkg, 0, sizeof(pkg));
|
|
memset(&playgo, 0, sizeof(playgo));
|
|
meta.uri = uris[i];
|
|
rc2 = ai_install_by_package(&meta, &pkg, &playgo);
|
|
{
|
|
size_t l = strlen(tries);
|
|
snprintf(tries + l, sizeof(tries) - l, "%s%s=0x%08x",
|
|
l ? "," : "", labels[i], (unsigned)rc2);
|
|
}
|
|
if (rc2 == 0) {
|
|
snprintf(msg, msg_sz, "install started (InstallByPackage%s)",
|
|
pkg_tid_mismatch ? ", shared master bytes" : "");
|
|
return 0;
|
|
}
|
|
}
|
|
rc = rc2;
|
|
if (pkg_tid_mismatch) {
|
|
snprintf(msg, msg_sz,
|
|
"install rejected (pkg %.12s -> %.12s; tries: %s)",
|
|
pkg_tid, expected_title_id ? expected_title_id : "", tries);
|
|
return rc ? rc : -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Last resort for normal same-title packages only. This path has no target
|
|
metadata parameter, so it is intentionally skipped for shared-master
|
|
region bytes. */
|
|
{
|
|
ai_pkg_info_t pkg = {0};
|
|
int rc2 = ai_install_pkg(sdk_path, &pkg);
|
|
if (rc2 == 0) {
|
|
snprintf(msg, msg_sz, "install started (AppInstallPkg)");
|
|
return 0;
|
|
}
|
|
snprintf(msg, msg_sz,
|
|
"install rejected (InstallByPackage=0x%08x, AppInstallPkg=0x%08x)",
|
|
(unsigned)rc, (unsigned)rc2);
|
|
return rc2 ? rc2 : (rc ? rc : -1);
|
|
}
|
|
}
|