mirror of
https://github.com/TheCriicom/omega-dashboard-ps5.git
synced 2026-10-17 10:01:25 +02:00
921 lines
47 KiB
C
921 lines
47 KiB
C
// Omega UI — installazione degli homebrew dello Store, secondo il tipo di file:
|
|
// · .zip con <Nome>/homebrew.js o eboot.elf (formato websrv) → /data/homebrew;
|
|
// · .zip con homebrew.js in radice → /data/homebrew/<Nome>;
|
|
// · .elf → Payload Manager, con il .elf.json accanto;
|
|
// · .zip di un'app con Title ID (anche dentro una sottocartella) → /user/app/<TID>
|
|
// e registrazione del titolo;
|
|
// · .pkg → sceAppInstUtilInstallByPackage: la console lo scarica da sé;
|
|
// · cartella di un gioco (caricata dal telefono o dal PC, file:///data/...) →
|
|
// /user/app/<TID> e registrazione del titolo.
|
|
// I file caricati in La mia libreria (url file://) si usano dove sono, senza scaricarli.
|
|
// Le prime tre scrivono solo file; pkg e registrazione dei titoli richiedono il
|
|
// privilegio ShellCore, e senza si indica un ripiego (ItemzFlow).
|
|
// Sul desktop si lavora nel finto filesystem OMEGA_SYSROOT (serve minizip).
|
|
#include "app.h"
|
|
#include <fcntl.h>
|
|
#include <math.h>
|
|
#include <stdint.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
#include <dirent.h>
|
|
|
|
#if defined(PS5) || defined(OMEGA_HAVE_MINIZIP)
|
|
#define HAVE_ZIP 1
|
|
#include <minizip/unzip.h>
|
|
#endif
|
|
|
|
#ifdef PS5
|
|
#include <ps5/kernel.h>
|
|
#include <arpa/inet.h>
|
|
#include <netinet/in.h>
|
|
#include <sys/socket.h>
|
|
// Layout corretti (websrv PR #51, etaHEN common_utils.h, firma di ShellUI):
|
|
// i metadati hanno anche slot e is_playgo_enabled (0x38 byte) e PlayGoInfo è
|
|
// di 0x2700 byte. Con le strutture vecchie, più piccole, i giochi base
|
|
// fallivano con 0x80B2116F (SCE_PLAYGO_ERROR_CORE_INVALID_SLOT).
|
|
typedef struct { const char *uri, *ex_uri, *playgo_scenario_id, *content_id, *content_name, *icon_url; uint32_t slot, is_playgo_enabled; } pkg_metadata_t;
|
|
typedef struct { char content_id[0x30]; int type; int platform; } pkg_info_t;
|
|
typedef struct { char lang[30][8]; char scenario_ids[64][3]; char content_ids[64][0x30]; long unknown[810]; } playgo_info_t;
|
|
_Static_assert(sizeof(pkg_metadata_t) == 0x38, "MetaInfo");
|
|
_Static_assert(sizeof(playgo_info_t) == 0x2700, "PlayGoInfo");
|
|
typedef struct { int32_t error_code; int32_t version; char description[512]; char type[9]; } inst_error_t;
|
|
typedef struct {
|
|
char status[16]; char src_type[8]; uint32_t remain_time;
|
|
uint64_t downloaded_size, initial_chunk_size, total_size;
|
|
uint32_t promote_progress; inst_error_t error_info;
|
|
int32_t local_copy_percent; char is_copy_only;
|
|
char pad[256]; // margine: firmware diversi possono scrivere di più
|
|
} inst_status_t;
|
|
int sceAppInstUtilInitialize(void);
|
|
int sceAppInstUtilInstallByPackage(const pkg_metadata_t *, pkg_info_t *, playgo_info_t *);
|
|
int sceAppInstUtilGetInstallStatus(const char *content_id, inst_status_t *st);
|
|
int sceAppInstUtilAppInstallAll(void *);
|
|
int sceAppInstUtilAppUnInstall(const char *);
|
|
#endif
|
|
#define APP_DIR OMEGA_SYSROOT "/user/app"
|
|
#define ZIP_CHUNK (256 * 1024)
|
|
#define CANCELLED (-102) // omega_url_download: interrotto con *cancel
|
|
|
|
// -------------------------------------------------------------------- stato --
|
|
enum { KIND_AUTO = 0, KIND_PKG = 1, KIND_ZIP = 2, KIND_ELF = 3, KIND_FOLDER = 4, KIND_HTML = 99 };
|
|
|
|
static SDL_atomic_t g_state; // 0 fermo, 1 in corso, 2 finito
|
|
static volatile long g_done, g_total; // byte scaricati / totali
|
|
static volatile int g_cancel;
|
|
static char g_label[96], g_phase[128], g_result[512];
|
|
static volatile int g_dl; // fase di scaricamento: avanzamento in MB
|
|
static int g_result_err, g_installed_title;
|
|
static Uint32 g_started;
|
|
static char g_icon[320]; // icona della tessera in home (file locale o URL)
|
|
static char g_libid[40]; // voce di La mia libreria da togliere a fine lavoro (file caricati)
|
|
static Uint32 g_ended; static char g_end_name[96], g_end_icon[320];
|
|
#define QMAX 16
|
|
static InstallReq g_queue[QMAX]; static int g_nq; // installazioni in attesa (solo thread principale)
|
|
|
|
int install_busy(void) { return SDL_AtomicGet(&g_state) == 1; }
|
|
|
|
int store_uninstall(const char *title_id) {
|
|
#ifdef PS5
|
|
if (!title_id || !title_id[0]) return -1;
|
|
sceAppInstUtilInitialize();
|
|
return sceAppInstUtilAppUnInstall(title_id);
|
|
#else
|
|
(void)title_id; return 0;
|
|
#endif
|
|
}
|
|
|
|
// ------------------------------------------------------------------ utilità --
|
|
// crea le cartelle che contengono path (mkdir -p della directory)
|
|
static void mkparents(const char *path) {
|
|
char tmp[700]; snprintf(tmp, sizeof tmp, "%s", path);
|
|
for (char *p = tmp + 1; *p; p++) if (*p == '/') { *p = 0; mkdir(tmp, 0755); *p = '/'; }
|
|
}
|
|
static void mkdirs(const char *dir) { char t[700]; snprintf(t, sizeof t, "%s/", dir); mkparents(t); }
|
|
#ifdef HAVE_ZIP
|
|
// cartelle di un'app aperte a tutti, come i file (vedi unzip_to)
|
|
static void chmod_dirs(const char *dir, int depth) {
|
|
chmod(dir, 0777);
|
|
if (depth > 12) return;
|
|
DIR *d = opendir(dir); if (!d) return;
|
|
struct dirent *e;
|
|
while ((e = readdir(d))) {
|
|
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
|
char p[900]; snprintf(p, sizeof p, "%s/%s", dir, e->d_name);
|
|
struct stat st; if (lstat(p, &st) == 0 && S_ISDIR(st.st_mode)) chmod_dirs(p, depth + 1);
|
|
}
|
|
closedir(d);
|
|
}
|
|
#endif
|
|
|
|
// nome di cartella sicuro da un titolo: lettere, cifre, - e _
|
|
static void safe_name(const char *src, char *out, size_t n) {
|
|
size_t o = 0;
|
|
for (const unsigned char *p = (const unsigned char *)src; *p && o + 1 < n; p++)
|
|
if ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') || *p == '-' || *p == '_') out[o++] = (char)*p;
|
|
out[o] = 0;
|
|
if (!o) snprintf(out, n, "Homebrew");
|
|
}
|
|
|
|
// ultimo segmento del percorso dell'URL, senza query
|
|
static void url_basename(const char *url, char *out, size_t n) {
|
|
const char *q = strpbrk(url, "?#"); size_t L = q ? (size_t)(q - url) : strlen(url);
|
|
const char *s = url + L; while (s > url && s[-1] != '/') s--;
|
|
size_t k = (size_t)(url + L - s); if (k >= n) k = n - 1;
|
|
memcpy(out, s, k); out[k] = 0;
|
|
for (char *c = out; *c; c++) if (*c == '/' || *c == '\\') *c = '_';
|
|
if (!out[0]) snprintf(out, n, "payload.elf");
|
|
}
|
|
|
|
static int ends_with(const char *s, const char *suf) { size_t a = strlen(s), b = strlen(suf); return a >= b && !strcasecmp(s + a - b, suf); }
|
|
|
|
static void json_str_field(FILE *f, const char *key, const char *val, int last) {
|
|
char esc[700]; json_escape(esc, sizeof esc, val ? val : "");
|
|
fprintf(f, " \"%s\": \"%s\"%s\n", key, esc, last ? "" : ",");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------- zip --
|
|
#ifdef HAVE_ZIP
|
|
// percorsi assoluti e ".." si saltano (zip-slip)
|
|
static int unsafe_entry(const char *name) {
|
|
if (!name[0] || name[0] == '/' || name[0] == '\\') return 1;
|
|
return strstr(name, "..") != NULL;
|
|
}
|
|
|
|
typedef struct { char top[256], app[256]; int hb_top, hb_root, files, app_found; } ZipInfo;
|
|
|
|
// cartella comune di primo livello, posizione di homebrew.js / eboot.elf e
|
|
// cartella dell'app (quella con sce_sys/param.json, la meno profonda): alcuni
|
|
// zip la mettono un livello più giù, accanto a strumenti per il PC
|
|
// (PSXS5-v2.2.0/PPSA97510/...)
|
|
static int zip_scan(const char *zip, ZipInfo *zi) {
|
|
memset(zi, 0, sizeof *zi);
|
|
unzFile uf = unzOpen64(zip); if (!uf) return -1;
|
|
int first = 1, common = 1;
|
|
if (unzGoToFirstFile(uf) == UNZ_OK) do {
|
|
unz_file_info64 fi; char nm[512];
|
|
if (unzGetCurrentFileInfo64(uf, &fi, nm, sizeof nm, NULL, 0, NULL, 0) != UNZ_OK) continue;
|
|
zi->files++;
|
|
const char *slash = strchr(nm, '/');
|
|
if (!slash) common = 0;
|
|
else if (first) { snprintf(zi->top, sizeof zi->top, "%.*s", (int)(slash - nm) + 1, nm); first = 0; }
|
|
else if (strncmp(nm, zi->top, strlen(zi->top))) common = 0;
|
|
if (!strcmp(nm, "homebrew.js") || !strcmp(nm, "eboot.elf")) zi->hb_root = 1;
|
|
if (slash && strchr(slash + 1, '/') == NULL && (!strcmp(slash + 1, "homebrew.js") || !strcmp(slash + 1, "eboot.elf"))) zi->hb_top = 1;
|
|
size_t nl = strlen(nm), pl = strlen("sce_sys/param.json");
|
|
if (nl >= pl && !strcmp(nm + nl - pl, "sce_sys/param.json") && (nl == pl || nm[nl - pl - 1] == '/')
|
|
&& (!zi->app_found || nl - pl < strlen(zi->app))) {
|
|
snprintf(zi->app, sizeof zi->app, "%.*s", (int)(nl - pl), nm); zi->app_found = 1;
|
|
}
|
|
} while (unzGoToNextFile(uf) == UNZ_OK);
|
|
unzClose(uf);
|
|
if (!common) { zi->top[0] = 0; zi->hb_top = 0; }
|
|
return zi->files ? 0 : -1;
|
|
}
|
|
|
|
// estrae in dest togliendo il prefisso strip dai nomi; con only si salta
|
|
// quello che sta fuori da strip, con app i file restano leggibili ed eseguibili
|
|
// da tutti (un'app con permessi stretti non parte: CE-107750-0)
|
|
static int unzip_to(const char *zip, const char *dest, const char *strip, int only, int app) {
|
|
unzFile uf = unzOpen64(zip); if (!uf) return -1;
|
|
size_t striplen = strip ? strlen(strip) : 0;
|
|
int rc = 0, files = 0;
|
|
char *buf = malloc(ZIP_CHUNK); if (!buf) { unzClose(uf); return -1; }
|
|
if (unzGoToFirstFile(uf) == UNZ_OK) do {
|
|
unz_file_info64 fi; char nm[512];
|
|
if (unzGetCurrentFileInfo64(uf, &fi, nm, sizeof nm, NULL, 0, NULL, 0) != UNZ_OK) { rc = -1; break; }
|
|
if (unsafe_entry(nm)) continue;
|
|
if (only && striplen && strncmp(nm, strip, striplen)) continue;
|
|
const char *rel = (striplen && !strncmp(nm, strip, striplen)) ? nm + striplen : nm;
|
|
if (!rel[0]) continue;
|
|
char out[900]; snprintf(out, sizeof out, "%s/%s", dest, rel);
|
|
size_t L = strlen(out);
|
|
if (L && out[L - 1] == '/') { mkparents(out); continue; }
|
|
mkparents(out);
|
|
if (unzOpenCurrentFile(uf) != UNZ_OK) { rc = -1; break; }
|
|
FILE *f = fopen(out, "wb");
|
|
if (!f) { unzCloseCurrentFile(uf); rc = -1; break; }
|
|
int k;
|
|
while ((k = unzReadCurrentFile(uf, buf, ZIP_CHUNK)) > 0) if (fwrite(buf, 1, (size_t)k, f) != (size_t)k) { k = -1; break; }
|
|
fclose(f); unzCloseCurrentFile(uf);
|
|
if (k < 0) { rc = -1; break; }
|
|
// gli eseguibili devono restare tali (anche i file senza estensione)
|
|
if (app) chmod(out, 0777);
|
|
else if (ends_with(out, ".elf") || ends_with(out, ".so") || !strchr(rel, '.')) chmod(out, 0755);
|
|
files++;
|
|
snprintf(g_phase, sizeof g_phase, _("Estrazione (%d file)"), files);
|
|
} while (unzGoToNextFile(uf) == UNZ_OK);
|
|
free(buf); unzClose(uf);
|
|
return rc ? rc : (files ? 0 : -1);
|
|
}
|
|
#endif
|
|
|
|
#ifdef PS5
|
|
// Registra un titolo già estratto su disco. sceAppInstUtilAppInstallTitleDir non
|
|
// è esportata dall'SDK: si risolve per NID, come fa ftpsrv.
|
|
static int reg_app(const char *title_id, const char *dir) {
|
|
int (*AppInstallTitleDir)(const char *, const char *, void *) = 0;
|
|
uint32_t handle;
|
|
if (!kernel_dynlib_handle(-1, "libSceAppInstUtil.sprx", &handle))
|
|
AppInstallTitleDir = (void *)kernel_dynlib_resolve(-1, handle, "Wudg3Xe3heE");
|
|
if (AppInstallTitleDir) return AppInstallTitleDir(title_id, dir, 0);
|
|
return sceAppInstUtilAppInstallAll(0);
|
|
}
|
|
#endif
|
|
|
|
#ifdef PS5
|
|
// per drives.c: registrare un gioco esterno montato
|
|
int app_register(const char *title_id, const char *dir) { return reg_app(title_id, dir); }
|
|
#endif
|
|
|
|
// ------------------------------------------------------ riconoscimento tipo --
|
|
// file già sulla console (caricato da La mia libreria): file:///data/...
|
|
static const char *local_path(const char *url) { return !strncmp(url, "file://", 7) ? url + 7 : NULL; }
|
|
|
|
static int detect_kind(int kind, const char *url) {
|
|
if (kind == KIND_PKG || kind == KIND_ZIP || kind == KIND_ELF || kind == KIND_FOLDER) return kind;
|
|
if (local_path(url)) { struct stat st; if (stat(local_path(url), &st) == 0 && S_ISDIR(st.st_mode)) return KIND_FOLDER; }
|
|
const char *q = strpbrk(url, "?#"); size_t L = q ? (size_t)(q - url) : strlen(url);
|
|
if (L >= 4 && !strncasecmp(url + L - 4, ".pkg", 4)) return KIND_PKG;
|
|
if (L >= 4 && !strncasecmp(url + L - 4, ".zip", 4)) return KIND_ZIP;
|
|
if (L >= 4 && !strncasecmp(url + L - 4, ".elf", 4)) return KIND_ELF;
|
|
unsigned char m[8] = { 0 };
|
|
long got = -1;
|
|
if (local_path(url)) { int fd = open(local_path(url), O_RDONLY); if (fd >= 0) { got = read(fd, m, sizeof m); close(fd); } }
|
|
else got = omega_url_peek(url, m, sizeof m);
|
|
if (got >= 4) {
|
|
if (m[0] == 'P' && m[1] == 'K' && m[2] == 3 && m[3] == 4) return KIND_ZIP;
|
|
if (m[0] == 0x7F && m[1] == 'E' && m[2] == 'L' && m[3] == 'F') return KIND_ELF;
|
|
if (m[0] == 0x7F && m[1] == 'C' && m[2] == 'N' && m[3] == 'T') return KIND_PKG; // pkg PS4/PS5
|
|
// una pagina web al posto del file (Drive, Mega, pagine di download...)
|
|
int i = 0; while (i < got && (m[i] == ' ' || m[i] == '\n' || m[i] == '\r' || m[i] == '\t' || m[i] == 0xEF || m[i] == 0xBB || m[i] == 0xBF)) i++;
|
|
if (i < got && m[i] == '<') return KIND_HTML;
|
|
}
|
|
return KIND_PKG;
|
|
}
|
|
|
|
// ------------------------------------------------------------------- lavori --
|
|
static int copy_file(const char *src, const char *dst) {
|
|
int in = open(src, O_RDONLY); if (in < 0) return -1;
|
|
int out = open(dst, O_WRONLY | O_CREAT | O_TRUNC, 0666); if (out < 0) { close(in); return -1; }
|
|
struct stat st; if (fstat(in, &st) == 0) g_total = (long)st.st_size;
|
|
static char buf[ZIP_CHUNK]; ssize_t k; int rc = 0;
|
|
while ((k = read(in, buf, sizeof buf)) > 0) {
|
|
if (g_cancel || write(out, buf, (size_t)k) != k) { rc = -1; break; }
|
|
g_done += (long)k;
|
|
}
|
|
if (k < 0) rc = -1;
|
|
close(in); close(out);
|
|
if (rc) unlink(dst);
|
|
return rc;
|
|
}
|
|
// cartella intera (quando non si può spostare: un'altra partizione)
|
|
static int copy_tree(const char *src, const char *dst) {
|
|
struct stat st; if (stat(src, &st) != 0) return -1;
|
|
if (!S_ISDIR(st.st_mode)) return copy_file(src, dst);
|
|
mkdir(dst, 0777);
|
|
DIR *d = opendir(src); if (!d) return -1;
|
|
struct dirent *e; int rc = 0;
|
|
while (!rc && (e = readdir(d))) {
|
|
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
|
if (g_cancel) { rc = -1; break; }
|
|
char a[1024], b[1024]; snprintf(a, sizeof a, "%s/%s", src, e->d_name); snprintf(b, sizeof b, "%s/%s", dst, e->d_name);
|
|
rc = copy_tree(a, b);
|
|
}
|
|
closedir(d);
|
|
return rc;
|
|
}
|
|
|
|
#ifdef PS5
|
|
static void copy_tree_rm(const char *p) {
|
|
struct stat st; if (lstat(p, &st) != 0) return;
|
|
if (S_ISDIR(st.st_mode)) {
|
|
DIR *d = opendir(p); struct dirent *e;
|
|
while (d && (e = readdir(d))) { if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue; char c[1024]; snprintf(c, sizeof c, "%s/%s", p, e->d_name); copy_tree_rm(c); }
|
|
if (d) closedir(d);
|
|
rmdir(p);
|
|
} else unlink(p);
|
|
}
|
|
#endif
|
|
|
|
static int download(const InstallReq *j, const char *dest) {
|
|
mkparents(dest);
|
|
if (local_path(j->url)) { // già sulla console: basta copiarlo
|
|
snprintf(g_phase, sizeof g_phase, "%s", _("Copia sulla console")); g_dl = 1;
|
|
if (copy_file(local_path(j->url), dest) == 0) return 0;
|
|
g_dl = 0;
|
|
snprintf(g_result, sizeof g_result, "%s", g_cancel ? _("Installazione annullata") : _("Copia non riuscita"));
|
|
return g_cancel ? -2 : -1;
|
|
}
|
|
snprintf(g_phase, sizeof g_phase, "%s", _("Scaricamento")); g_dl = 1;
|
|
int st = omega_url_download(j->url, dest, &g_done, &g_total, &g_cancel);
|
|
if (st == 200) return 0;
|
|
unlink(dest);
|
|
g_dl = 0;
|
|
if (st == CANCELLED) snprintf(g_result, sizeof g_result, "%s", _("Installazione annullata"));
|
|
else if (st == -103) snprintf(g_result, sizeof g_result, "%s", _("Download interrotto a metà (connessione caduta): riprova"));
|
|
else if (st == 404 || st == 403 || st == 410) snprintf(g_result, sizeof g_result, _("Il link non funziona più (HTTP %d): aggiornalo nella libreria"), st);
|
|
else snprintf(g_result, sizeof g_result, _("Download non riuscito (%d)"), st);
|
|
return st == CANCELLED ? -2 : -1;
|
|
}
|
|
|
|
#ifdef PS5
|
|
// codici dell'installatore di sistema (tabella di etaHEN) → una frase
|
|
static const char *pkg_error(int rc) {
|
|
switch ((unsigned)rc) {
|
|
case 0x80A30002u: return _("spazio esaurito sulla console");
|
|
case 0x80A30003u: return _("pkg non valido o link sbagliato");
|
|
case 0x80A30006u: return _("pkg non installabile su questa console (tipo o licenza)");
|
|
case 0x80A30008u: return _("pkg danneggiato o scaricato a metà");
|
|
case 0x80A3000Cu: return _("il gioco è aperto: chiudilo e riprova");
|
|
case 0x80A3000Du: return _("serve un firmware più recente");
|
|
case 0x80A3000Fu: return _("il pkg non corrisponde al gioco già installato");
|
|
case 0x80A30015u: return _("c'è già un'installazione in corso nel sistema");
|
|
case 0x80A31000u: return _("installatore di sistema non pronto: riavvia Omega");
|
|
}
|
|
return NULL;
|
|
}
|
|
static void pkg_fail(int rc) {
|
|
const char *why = pkg_error(rc);
|
|
if (why) snprintf(g_result, sizeof g_result, _("Installazione non riuscita: %s (0x%08X)"), why, (unsigned)rc);
|
|
else snprintf(g_result, sizeof g_result, _("Installazione pkg non riuscita (0x%08X). Ripiego: ItemzFlow."), (unsigned)rc);
|
|
}
|
|
|
|
// spazi e simboli nell'URL: l'installatore di sistema li vuole codificati
|
|
static void url_clean(const char *in, char *out, size_t n) {
|
|
size_t o = 0;
|
|
for (const unsigned char *p = (const unsigned char *)in; *p && o + 4 < n; p++) {
|
|
if (*p == ' ' || *p == '"' || *p == '<' || *p == '>' || *p == '[' || *p == ']' || *p >= 0x80) o += (size_t)snprintf(out + o, n - o, "%%%02X", *p);
|
|
else out[o++] = (char)*p;
|
|
}
|
|
out[o] = 0;
|
|
}
|
|
#endif
|
|
|
|
// ------------------------------------------------------------ dentro il pkg --
|
|
static uint32_t rbe32(const unsigned char *p) { return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | p[3]; }
|
|
static void sfo_field(const unsigned char *b, uint32_t len, const char *key, char *out, size_t n) {
|
|
if (len < 20 || memcmp(b, "\0PSF", 4)) return;
|
|
uint32_t kt = b[8] | b[9] << 8 | b[10] << 16 | (uint32_t)b[11] << 24, dt = b[12] | b[13] << 8 | b[14] << 16 | (uint32_t)b[15] << 24, cnt = b[16] | b[17] << 8 | b[18] << 16 | (uint32_t)b[19] << 24;
|
|
for (uint32_t i = 0; i < cnt && 20 + i * 16 + 16 <= len; i++) {
|
|
const unsigned char *e = b + 20 + i * 16;
|
|
uint32_t ko = e[0] | e[1] << 8, dl = e[4] | e[5] << 8 | e[6] << 16 | (uint32_t)e[7] << 24, doff = e[12] | e[13] << 8 | e[14] << 16 | (uint32_t)e[15] << 24;
|
|
if (kt + ko >= len || dt + doff + dl > len) continue;
|
|
if (!strcmp((const char *)b + kt + ko, key)) { snprintf(out, n, "%.*s", (int)dl, (const char *)b + dt + doff); return; }
|
|
}
|
|
}
|
|
// Titolo, Content ID e icona di un pkg PS4 (intestazione "\x7fCNT": tabella
|
|
// delle voci con param.sfo = 0x1000 e icon0.png = 0x1200). icon_dest può essere
|
|
// NULL. 1 = riconosciuto.
|
|
int pkg_info(const char *path, char *title, size_t tn, char *cid, size_t cn, const char *icon_dest) {
|
|
if (title && tn) title[0] = 0;
|
|
if (cid && cn) cid[0] = 0;
|
|
int fd = open(path, O_RDONLY); if (fd < 0) return 0;
|
|
unsigned char h[0x80]; int ok = 0;
|
|
if (read(fd, h, sizeof h) == (ssize_t)sizeof h && !memcmp(h, "\x7f" "CNT", 4)) {
|
|
ok = 1;
|
|
if (cid && cn) { snprintf(cid, cn, "%.36s", (const char *)h + 0x40); }
|
|
uint32_t count = rbe32(h + 0x10), table = rbe32(h + 0x18);
|
|
if (count && count < 4096) {
|
|
unsigned char *t = malloc((size_t)count * 32);
|
|
if (t && lseek(fd, table, SEEK_SET) == (off_t)table && read(fd, t, (size_t)count * 32) == (ssize_t)(count * 32)) {
|
|
for (uint32_t i = 0; i < count; i++) {
|
|
const unsigned char *e = t + i * 32;
|
|
uint32_t id = rbe32(e), off = rbe32(e + 16), size = rbe32(e + 20);
|
|
if (!size || size > 4 * 1024 * 1024) continue;
|
|
if ((id == 0x1000 && title && tn) || (id == 0x1200 && icon_dest)) {
|
|
unsigned char *b = malloc(size);
|
|
if (b && lseek(fd, off, SEEK_SET) == (off_t)off && read(fd, b, size) == (ssize_t)size) {
|
|
if (id == 0x1000) { sfo_field(b, size, "TITLE", title, tn); }
|
|
else { mkparents(icon_dest); FILE *f = fopen(icon_dest, "wb"); if (f) { fwrite(b, 1, size, f); fclose(f); } }
|
|
}
|
|
free(b);
|
|
}
|
|
}
|
|
}
|
|
free(t);
|
|
}
|
|
} else if (!memcmp(h, "\x7f" "FIH", 4)) ok = 2; // pkg PS5: niente da leggere qui
|
|
close(fd);
|
|
return ok;
|
|
}
|
|
|
|
#ifdef PS5
|
|
// AppInstUtil vuole il privilegio di ShellCore: Omega gira con quello
|
|
// dell'app di lancio, quindi per la durata della chiamata si prende in
|
|
// prestito l'authid di ShellCore (come fanno etaHEN e i caricatori di pkg).
|
|
#define AUTHID_SHELLCORE 0x3800000000000010ul
|
|
static uint64_t authid_push(void) { uint64_t old = kernel_get_ucred_authid(getpid()); kernel_set_ucred_authid(getpid(), AUTHID_SHELLCORE); return old; }
|
|
static void authid_pop(uint64_t old) { if (old) kernel_set_ucred_authid(getpid(), old); }
|
|
|
|
static int install_by_pkg(pkg_metadata_t *m, pkg_info_t *info) {
|
|
static playgo_info_t pg; // 10 KB: meglio fuori dallo stack del thread
|
|
memset(info, 0, sizeof *info); memset(&pg, 0, sizeof pg);
|
|
uint64_t a = authid_push();
|
|
int rc = sceAppInstUtilInstallByPackage(m, info, &pg);
|
|
authid_pop(a);
|
|
return rc;
|
|
}
|
|
|
|
// /data è un collegamento: l'installatore vuole il percorso vero /user/data
|
|
static void real_uri(const char *in, char *out, size_t n) {
|
|
if (!strncmp(in, "/data/", 6)) snprintf(out, n, "/user%s", in); else snprintf(out, n, "%s", in);
|
|
}
|
|
|
|
// etaHEN Direct Package Installer v2 (porta 12800), se c'è: ripiego quando
|
|
// AppInstUtil rifiuta la chiamata. "SUCCESS..." = accodato.
|
|
static int dpi_install(const char *uri) {
|
|
int s = socket(AF_INET, SOCK_STREAM, 0); if (s < 0) return -1;
|
|
struct sockaddr_in a; memset(&a, 0, sizeof a); a.sin_family = AF_INET; a.sin_port = htons(12800); a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
struct timeval tv = { 20, 0 }; setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
|
|
if (connect(s, (struct sockaddr *)&a, sizeof a) != 0) { close(s); return -1; }
|
|
char body[1300], enc[1200]; url_encode(enc, sizeof enc, uri, "/:._-");
|
|
snprintf(body, sizeof body, "url=%s", enc);
|
|
char req[1700]; int L = snprintf(req, sizeof req, "POST / HTTP/1.1\r\nHost: 127.0.0.1\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s", (int)strlen(body), body);
|
|
int ok = write(s, req, (size_t)L) == L;
|
|
char resp[1024] = ""; int got = 0, k;
|
|
while (ok && got < (int)sizeof resp - 1 && (k = (int)read(s, resp + got, sizeof resp - 1 - got)) > 0) got += k;
|
|
resp[got] = 0; close(s);
|
|
omega_log("install: etaHEN DPI -> %.120s", resp);
|
|
return strstr(resp, "SUCCESS") ? 0 : -2;
|
|
}
|
|
|
|
// Avanzamento vero dell'installazione di sistema, come sulla PS4: si legge
|
|
// lo stato del Content ID finché non è "playable" (o un errore).
|
|
static int pkg_watch(const char *cid) {
|
|
if (!cid[0]) return 0;
|
|
snprintf(g_phase, sizeof g_phase, "%s", _("Installazione")); g_dl = 1; g_done = 0; g_total = 0;
|
|
inst_status_t st; int misses = 0, seen = 0; long last_done = -1; Uint32 still = SDL_GetTicks();
|
|
for (;;) {
|
|
SDL_Delay(500);
|
|
memset(&st, 0, sizeof st);
|
|
uint64_t a = authid_push();
|
|
int rc = sceAppInstUtilGetInstallStatus(cid, &st);
|
|
authid_pop(a);
|
|
if (rc != 0) {
|
|
// finito e tolto dalla coda di sistema, oppure non ancora entrato
|
|
if (seen && ++misses > 6) return 0;
|
|
if (!seen && ++misses > 40) { omega_log("install: stato di %s non disponibile (0x%08X)", cid, (unsigned)rc); return 0; }
|
|
continue;
|
|
}
|
|
seen = 1; misses = 0;
|
|
st.status[sizeof st.status - 1] = 0;
|
|
if (st.error_info.error_code) {
|
|
const char *why = pkg_error(st.error_info.error_code);
|
|
if (why) snprintf(g_result, sizeof g_result, _("Installazione non riuscita: %s (0x%08X)"), why, (unsigned)st.error_info.error_code);
|
|
else snprintf(g_result, sizeof g_result, _("Installazione non riuscita (0x%08X)"), (unsigned)st.error_info.error_code);
|
|
return -1;
|
|
}
|
|
if (st.total_size) { g_total = (long)(st.total_size / 1024); g_done = (long)(st.downloaded_size / 1024); }
|
|
if (!strcmp(st.status, "promoting")) snprintf(g_phase, sizeof g_phase, "%s", _("Finalizzazione"));
|
|
if (!strcmp(st.status, "playable") || !strcmp(st.status, "finished") || !strcmp(st.status, "completed") ||
|
|
(st.total_size && st.downloaded_size >= st.total_size && st.promote_progress >= 100)) { g_done = g_total; return 0; }
|
|
if (g_done != last_done) { last_done = g_done; still = SDL_GetTicks(); }
|
|
// fermo da 10 minuti: si smette di guardare, il sistema continua da solo
|
|
if (SDL_GetTicks() - still > 600000) { snprintf(g_result, sizeof g_result, "%s", _("L'installazione continua in background: la trovi nella Home della console")); return 1; }
|
|
}
|
|
}
|
|
#endif
|
|
|
|
static int do_pkg(const InstallReq *j) {
|
|
const char *lp = local_path(j->url);
|
|
// icona e titolo dal pkg stesso, per la tessera in home
|
|
if (lp) {
|
|
char t[96], cid[48], ic[300];
|
|
snprintf(ic, sizeof ic, OMEGA_DIR "/dl/icons/%08x.png", fnv1a(lp));
|
|
if (pkg_info(lp, t, sizeof t, cid, sizeof cid, ic)) {
|
|
if (access(ic, 0) == 0 && !g_icon[0]) snprintf(g_icon, sizeof g_icon, "%s", ic);
|
|
if (t[0] && !j->name[0]) snprintf(g_label, sizeof g_label, "%s", t);
|
|
}
|
|
}
|
|
#ifdef PS5
|
|
if (sceAppInstUtilInitialize()) { snprintf(g_result, sizeof g_result, "%s", _("Installatore di sistema non disponibile: riavvia Omega. Ripiego: installa il pkg con ItemzFlow.")); return -1; }
|
|
char remote[1100]; url_clean(j->url, remote, sizeof remote);
|
|
char uri_buf[760]; const char *uri = remote;
|
|
if (lp) {
|
|
// l'installatore di sistema rifiuta i percorsi con spazi e simboli: accanto al
|
|
// file si crea un collegamento con un nome pulito e si installa da lì
|
|
char clean[700]; snprintf(clean, sizeof clean, "%s", lp);
|
|
if (strpbrk(lp, " []()'&#%")) {
|
|
char *base = strrchr(clean, '/'); base = base ? base + 1 : clean;
|
|
for (char *c = base; *c; c++) if (!((*c >= 'a' && *c <= 'z') || (*c >= 'A' && *c <= 'Z') || (*c >= '0' && *c <= '9') || *c == '.' || *c == '-' || *c == '_')) *c = '_';
|
|
struct stat cst; // mai cancellare: se il nome pulito c'è già (tentativo precedente) si usa quello
|
|
if (!(stat(clean, &cst) == 0 || link(lp, clean) == 0 || rename(lp, clean) == 0)) snprintf(clean, sizeof clean, "%s", lp);
|
|
}
|
|
real_uri(clean, uri_buf, sizeof uri_buf); uri = uri_buf;
|
|
omega_log("install: pkg dal percorso %s", uri);
|
|
}
|
|
char cid[48] = ""; if (lp) pkg_info(lp, NULL, 0, cid, sizeof cid, NULL);
|
|
snprintf(g_phase, sizeof g_phase, "%s", _("Avvio dell'installazione"));
|
|
pkg_metadata_t meta = { .uri = uri, .ex_uri = "", .playgo_scenario_id = "", .content_id = "", .content_name = g_label, .icon_url = "", .slot = 0, .is_playgo_enabled = 0 };
|
|
pkg_info_t info;
|
|
int rc = install_by_pkg(&meta, &info);
|
|
if (rc && lp) { // alcune versioni vogliono l'URI file://
|
|
char furi[780]; snprintf(furi, sizeof furi, "file://%s", uri);
|
|
omega_log("install: pkg rifiutato (0x%08X), riprovo come %s", (unsigned)rc, furi);
|
|
meta.uri = furi; rc = install_by_pkg(&meta, &info);
|
|
meta.uri = uri;
|
|
}
|
|
char dest[300] = "";
|
|
if (rc && !lp && rc != (int)0x80A30002 && rc != (int)0x80A3000D) {
|
|
// il sistema non ha accettato il link (redirect, https, server lento...):
|
|
// si scarica il pkg sulla console e si installa dal file
|
|
omega_log("install: link rifiutato (0x%08X), scarico il pkg sulla console", (unsigned)rc);
|
|
snprintf(dest, sizeof dest, OMEGA_DIR "/dl/omega-install-%u.pkg", (unsigned)SDL_GetTicks());
|
|
int d = download(j, dest);
|
|
if (d) return d;
|
|
char ic[300]; snprintf(ic, sizeof ic, OMEGA_DIR "/dl/icons/%08x.png", fnv1a(dest));
|
|
pkg_info(dest, NULL, 0, cid, sizeof cid, ic);
|
|
if (!g_icon[0] && access(ic, 0) == 0) snprintf(g_icon, sizeof g_icon, "%s", ic);
|
|
snprintf(g_phase, sizeof g_phase, "%s", _("Avvio dell'installazione"));
|
|
real_uri(dest, uri_buf, sizeof uri_buf);
|
|
meta.uri = uri_buf; rc = install_by_pkg(&meta, &info);
|
|
}
|
|
// ultimo ripiego: il Direct Package Installer di etaHEN, se è attivo
|
|
if (rc && rc != (int)0x80A30002 && rc != (int)0x80A3000D && rc != (int)0x80A3000C && dpi_install(meta.uri) == 0) {
|
|
omega_log("install: AppInstUtil 0x%08X, accodato da etaHEN DPI", (unsigned)rc);
|
|
rc = 0;
|
|
}
|
|
if (rc) { pkg_fail(rc); if (dest[0]) unlink(dest); return -1; }
|
|
if (!cid[0]) snprintf(cid, sizeof cid, "%.36s", info.content_id);
|
|
omega_log("install: pkg accodato, content id %s", cid);
|
|
int w = pkg_watch(cid);
|
|
if (w < 0) { if (dest[0]) unlink(dest); return -1; }
|
|
if (w == 0) {
|
|
// finito: il pkg scaricato non serve più
|
|
if (dest[0]) unlink(dest);
|
|
snprintf(g_result, sizeof g_result, "%s", _("Installato: lo trovi nella Home"));
|
|
g_installed_title = 1;
|
|
}
|
|
return 0;
|
|
#else
|
|
for (int i = 0; i <= 100 && !g_cancel; i += 2) { g_total = 100; g_done = i; g_dl = 1; snprintf(g_phase, sizeof g_phase, "%s", i < 70 ? _("Installazione") : _("Finalizzazione")); SDL_Delay(40); }
|
|
snprintf(g_result, sizeof g_result, "(desktop) pkg: sulla console va ad AppInstUtil — %s", j->name);
|
|
g_installed_title = 1;
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
// .elf → nella cartella del caricatore rilevato (hen.c): OnionHEN / etaHEN 2
|
|
// in una cartella piatta, altrimenti Payload Manager con il suo .elf.json.
|
|
// L'avvio automatico non si attiva da solo: lo decide l'utente nel caricatore.
|
|
static int do_elf(const InstallReq *j) {
|
|
char name[64], file[160], dir[400], path[600], tmp[620], side[640], sha[65];
|
|
safe_name(j->name, name, sizeof name);
|
|
// il nome vero del file: l'URL finale di GitHub è un link firmato senza nome
|
|
if (j->filename[0]) url_basename(j->filename, file, sizeof file); else url_basename(j->url, file, sizeof file);
|
|
if (!ends_with(file, ".elf")) { size_t L = strlen(file); snprintf(file + L, sizeof file - L, ".elf"); }
|
|
char flat[300];
|
|
if (hen_payload_dir(flat, sizeof flat)) {
|
|
snprintf(path, sizeof path, "%s/%s", flat, file);
|
|
snprintf(tmp, sizeof tmp, "%s.part", path);
|
|
mkdirs(flat);
|
|
int rc = download(j, tmp); if (rc) return rc;
|
|
if (rename(tmp, path) != 0) { unlink(tmp); snprintf(g_result, sizeof g_result, "%s", _("Impossibile salvare il payload")); return -1; }
|
|
chmod(path, 0755);
|
|
snprintf(side, sizeof side, "%s.omega.json", path); // solo il nome da mostrare in home
|
|
FILE *f = fopen(side, "w"); if (f) { fprintf(f, "{\n"); json_str_field(f, "name", j->name, 1); fprintf(f, "}\n"); fclose(f); }
|
|
snprintf(g_result, sizeof g_result, _("%s installato per %s: lo trovi in home e tra i payload del caricatore"), j->name, hen_name());
|
|
return 0;
|
|
}
|
|
snprintf(dir, sizeof dir, OMEGA_PLD_ROOT "/%s", name);
|
|
snprintf(path, sizeof path, "%s/%s", dir, file);
|
|
snprintf(tmp, sizeof tmp, "%s.part", path);
|
|
mkdirs(dir);
|
|
int rc = download(j, tmp); if (rc) return rc;
|
|
if (rename(tmp, path) != 0) { unlink(tmp); snprintf(g_result, sizeof g_result, "%s", _("Impossibile salvare il payload")); return -1; }
|
|
chmod(path, 0755);
|
|
snprintf(g_phase, sizeof g_phase, "%s", _("Verifica"));
|
|
if (file_sha256(path, sha)) sha[0] = 0;
|
|
snprintf(side, sizeof side, "%s.json", path);
|
|
FILE *f = fopen(side, "w");
|
|
if (f) {
|
|
char when[32]; time_t t = time(NULL); strftime(when, sizeof when, "%Y-%m-%dT%H:%M:%SZ", gmtime(&t));
|
|
fprintf(f, "{\n");
|
|
json_str_field(f, "name", j->name, 0);
|
|
json_str_field(f, "filename", file, 0);
|
|
json_str_field(f, "url", j->url, 0);
|
|
json_str_field(f, "source", j->source[0] ? j->source : j->url, 0);
|
|
json_str_field(f, "description", j->desc, 0);
|
|
json_str_field(f, "version", j->version, 0);
|
|
json_str_field(f, "checksum", sha, 0);
|
|
json_str_field(f, "category", j->category[0] ? j->category : "Utilities", 0);
|
|
json_str_field(f, "downloaded_at", when, 0);
|
|
json_str_field(f, "install_source", "Omega Store", 1);
|
|
fprintf(f, "}\n");
|
|
fclose(f);
|
|
}
|
|
if (ends_with(file, "-install.elf"))
|
|
snprintf(g_result, sizeof g_result, _("%s è in Payload Manager: avvialo una volta per completare l'installazione"), j->name);
|
|
else
|
|
snprintf(g_result, sizeof g_result, _("%s installato in Payload Manager"), j->name);
|
|
return 0;
|
|
}
|
|
|
|
static int do_zip(const InstallReq *j) {
|
|
#ifdef HAVE_ZIP
|
|
char zip[1100]; const char *lp = local_path(j->url);
|
|
int rc = 0;
|
|
if (lp) snprintf(zip, sizeof zip, "%s", lp);
|
|
else { snprintf(zip, sizeof zip, OMEGA_DIR "/dl/omega-install.zip"); rc = download(j, zip); if (rc) return rc; }
|
|
snprintf(g_phase, sizeof g_phase, "%s", _("Analisi"));
|
|
ZipInfo zi;
|
|
if (zip_scan(zip, &zi)) { if (!lp) unlink(zip); snprintf(g_result, sizeof g_result, "%s", _("Lo zip è vuoto o danneggiato")); return -1; }
|
|
if (zi.hb_top || zi.hb_root) {
|
|
char dest[400], folder[256];
|
|
if (zi.hb_top) { snprintf(folder, sizeof folder, "%.*s", (int)strlen(zi.top) - 1, zi.top); snprintf(dest, sizeof dest, "%s", OMEGA_HB_ROOT); }
|
|
else {
|
|
// homebrew.js in radice: la cartella prende il nome dello zip, come nelle
|
|
// istruzioni di websrv (YQuake2.zip → /data/homebrew/YQuake2), perché
|
|
// alcuni script cercano i propri file per percorso. Nomi con versioni
|
|
// ("pnes-7.1_ps5") o vuoti → il titolo dello Store.
|
|
char zn[160]; url_basename(j->filename[0] ? j->filename : j->url, zn, sizeof zn);
|
|
size_t L = strlen(zn); if (ends_with(zn, ".zip")) zn[L - 4] = 0;
|
|
if (zn[0] && !strchr(zn, '.') && !strchr(zn, '%')) safe_name(zn, folder, sizeof folder);
|
|
else safe_name(j->name, folder, sizeof folder);
|
|
snprintf(dest, sizeof dest, OMEGA_HB_ROOT "/%s", folder);
|
|
}
|
|
mkdirs(dest);
|
|
rc = unzip_to(zip, dest, "", 0, 0);
|
|
if (!lp) unlink(zip);
|
|
if (rc) { snprintf(g_result, sizeof g_result, "%s", _("Estrazione dello zip non riuscita")); return -1; }
|
|
omega_log("install: homebrew %s → %s/%s", j->name, OMEGA_HB_ROOT, folder);
|
|
snprintf(g_result, sizeof g_result, _("%s installato: lo trovi nella Home di Omega"), j->name);
|
|
g_installed_title = 1;
|
|
return 0;
|
|
}
|
|
if (!j->title_id[0]) { if (!lp) unlink(zip); snprintf(g_result, sizeof g_result, "%s", _("Zip non riconosciuto: manca homebrew.js/eboot.elf e non c'è un Title ID")); return -1; }
|
|
if (j->dest_mount[0]) {
|
|
// sul disco esterno: la cartella del gioco e basta; la home la trova da sola
|
|
// (drives.c) e la registra al primo avvio
|
|
char ed[300]; snprintf(ed, sizeof ed, "%s/homebrew/%s", j->dest_mount, j->title_id);
|
|
mkdirs(ed);
|
|
rc = unzip_to(zip, ed, zi.app_found ? zi.app : zi.top, zi.app_found, 1);
|
|
if (!rc) chmod_dirs(ed, 0);
|
|
if (!lp) unlink(zip);
|
|
if (rc) { snprintf(g_result, sizeof g_result, "%s", _("Estrazione dello zip non riuscita")); return -1; }
|
|
snprintf(g_result, sizeof g_result, _("%s installato su %s: lo trovi in home"), j->name[0] ? j->name : j->title_id, j->dest_label);
|
|
g_installed_title = 1;
|
|
return 0;
|
|
}
|
|
char dest[200]; snprintf(dest, sizeof dest, APP_DIR "/%s", j->title_id);
|
|
# ifdef PS5
|
|
sceAppInstUtilInitialize();
|
|
sceAppInstUtilAppUnInstall(j->title_id); // reinstallazione
|
|
# endif
|
|
mkdirs(dest);
|
|
rc = unzip_to(zip, dest, zi.app_found ? zi.app : zi.top, zi.app_found, 1);
|
|
if (!rc) chmod_dirs(dest, 0);
|
|
if (!lp) unlink(zip);
|
|
if (rc) { snprintf(g_result, sizeof g_result, "%s", _("Estrazione dello zip non riuscita")); return -1; }
|
|
# ifdef PS5
|
|
snprintf(g_phase, sizeof g_phase, "%s", _("Registrazione"));
|
|
rc = reg_app(j->title_id, "/user/app/");
|
|
if (rc) { snprintf(g_result, sizeof g_result, _("Registrazione non riuscita (0x%08X). Privilegio ShellCore mancante?"), (unsigned)rc); return -1; }
|
|
g_installed_title = 1;
|
|
# endif
|
|
snprintf(g_result, sizeof g_result, _("%s installato"), j->name[0] ? j->name : j->title_id);
|
|
return 0;
|
|
#else
|
|
(void)j;
|
|
for (int i = 0; i <= 100 && !g_cancel; i += 4) { g_total = 100; g_done = i; SDL_Delay(25); }
|
|
snprintf(g_result, sizeof g_result, "(desktop senza minizip) installazione zip simulata");
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
// cartella di un gioco caricata in La mia libreria → /user/app/<TID> e registrazione
|
|
static int do_folder(const InstallReq *j) {
|
|
const char *src = local_path(j->url);
|
|
if (!src || strlen(j->title_id) != 9) { snprintf(g_result, sizeof g_result, "%s", _("Cartella non valida: manca il Title ID")); return -1; }
|
|
char dest[200]; snprintf(dest, sizeof dest, APP_DIR "/%s", j->title_id);
|
|
struct stat sst, dst;
|
|
int have_src = stat(src, &sst) == 0 && S_ISDIR(sst.st_mode), have_dest = stat(dest, &dst) == 0 && S_ISDIR(dst.st_mode);
|
|
if (!have_src && !have_dest) { snprintf(g_result, sizeof g_result, "%s", _("I file del gioco non ci sono più: caricalo di nuovo dal telefono")); return -1; }
|
|
if (j->dest_mount[0]) {
|
|
// sul disco esterno: si COPIA (la cartella caricata resta), poi la home la trova da sola
|
|
if (!have_src) { snprintf(g_result, sizeof g_result, "%s", _("I file del gioco non ci sono più: caricalo di nuovo dal telefono")); return -1; }
|
|
char ed[300], eh[300]; snprintf(eh, sizeof eh, "%s/homebrew", j->dest_mount); snprintf(ed, sizeof ed, "%s/%s", eh, j->title_id);
|
|
mkdirs(eh);
|
|
snprintf(g_phase, sizeof g_phase, _("Copia su %s"), j->dest_label);
|
|
if (copy_tree(src, ed) != 0) { snprintf(g_result, sizeof g_result, "%s", g_cancel ? _("Installazione annullata") : _("Copia non riuscita: c'è spazio sul disco?")); return g_cancel ? -2 : -1; }
|
|
snprintf(g_result, sizeof g_result, _("%s installato su %s: lo trovi in home"), j->name[0] ? j->name : j->title_id, j->dest_label);
|
|
return 0;
|
|
}
|
|
if (have_src) {
|
|
// la cartella c'è: la versione installata si toglie solo adesso, non prima di
|
|
// sapere che c'è con cosa sostituirla (prima un secondo tentativo cancellava il gioco)
|
|
# ifdef PS5
|
|
sceAppInstUtilInitialize();
|
|
if (have_dest) sceAppInstUtilAppUnInstall(j->title_id); // reinstallazione
|
|
# endif
|
|
snprintf(g_phase, sizeof g_phase, "%s", _("Copia sulla console"));
|
|
mkdirs(APP_DIR);
|
|
// stessa partizione: si sposta e basta; altrimenti si copia
|
|
if (rename(src, dest) != 0 && copy_tree(src, dest) != 0) {
|
|
snprintf(g_result, sizeof g_result, "%s", g_cancel ? _("Installazione annullata") : _("Copia non riuscita"));
|
|
return g_cancel ? -2 : -1;
|
|
}
|
|
}
|
|
// la cartella era già stata spostata in /user/app (installazione precedente): si registra di nuovo
|
|
# ifdef PS5
|
|
snprintf(g_phase, sizeof g_phase, "%s", _("Registrazione"));
|
|
int rc = reg_app(j->title_id, "/user/app/");
|
|
if (rc) { snprintf(g_result, sizeof g_result, _("Registrazione non riuscita (0x%08X). Privilegio ShellCore mancante?"), (unsigned)rc); return -1; }
|
|
g_installed_title = 1;
|
|
# endif
|
|
omega_log("install: cartella %s → %s", src, dest);
|
|
snprintf(g_result, sizeof g_result, _("%s installato"), j->name[0] ? j->name : j->title_id);
|
|
return 0;
|
|
}
|
|
|
|
static int install_thread(void *arg) {
|
|
InstallReq *j = arg;
|
|
g_done = g_total = 0; g_cancel = 0; g_dl = 0; g_result[0] = 0; g_result_err = 0; g_installed_title = 0;
|
|
mkdir(OMEGA_DIR "/dl", 0777);
|
|
snprintf(g_phase, sizeof g_phase, "%s", _("Preparazione"));
|
|
mkdir(OMEGA_DIR, 0777);
|
|
int kind = detect_kind(j->kind, j->url);
|
|
omega_log("install: %s kind=%d %s", j->name, kind, j->url);
|
|
int rc;
|
|
if (kind == KIND_HTML) { snprintf(g_result, sizeof g_result, "%s", _("Il link porta a una pagina web, non al file: serve il link diretto al pkg, zip o elf")); rc = -1; }
|
|
else rc = kind == KIND_ZIP ? do_zip(j) : kind == KIND_ELF ? do_elf(j) : kind == KIND_FOLDER ? do_folder(j) : do_pkg(j);
|
|
g_result_err = rc == -1;
|
|
omega_log("install: %s -> %s", j->name, g_result);
|
|
free(j);
|
|
SDL_AtomicSet(&g_state, 2);
|
|
return 0;
|
|
}
|
|
|
|
// Dove installare: con un disco esterno collegato si chiede, per i giochi che
|
|
// Omega mette da sé (cartelle e zip con Title ID). I pkg li installa la console
|
|
// nella sua memoria, gli homebrew e i payload vanno dove li cerca il caricatore.
|
|
static InstallReq pending; static Drive pend_dr[12]; static int pend_nd;
|
|
static void install_start(const InstallReq *r);
|
|
static void dest_pick(int idx, void *ud) {
|
|
(void)ud;
|
|
if (idx < 0 || idx > pend_nd) return;
|
|
if (idx > 0) { snprintf(pending.dest_mount, sizeof pending.dest_mount, "%s", pend_dr[idx - 1].mount); snprintf(pending.dest_label, sizeof pending.dest_label, "%s", pend_dr[idx - 1].label); }
|
|
install_start(&pending);
|
|
}
|
|
|
|
void install_begin(const InstallReq *r) {
|
|
if (!r || !r->url[0]) { set_msg(_("Link di download mancante"), 1); return; }
|
|
int game = r->title_id[0] && r->kind != KIND_PKG && r->kind != KIND_ELF;
|
|
if (game && !r->dest_mount[0] && (pend_nd = drives_list(pend_dr, 12)) > 0) {
|
|
pending = *r;
|
|
static char lab[13][96]; static const char *it[13];
|
|
snprintf(lab[0], sizeof lab[0], "%s", _("Memoria della console")); it[0] = lab[0];
|
|
for (int i = 0; i < pend_nd && i < 12; i++) { snprintf(lab[i + 1], sizeof lab[0], _("%s · %.0f GB liberi"), pend_dr[i].label, pend_dr[i].free_gb); it[i + 1] = lab[i + 1]; }
|
|
menu_open(_("Dove lo installo?"), it, pend_nd + 1, dest_pick, NULL);
|
|
return;
|
|
}
|
|
install_start(r);
|
|
}
|
|
|
|
static void install_run(const InstallReq *r) {
|
|
InstallReq *j = malloc(sizeof *j);
|
|
if (!j) { set_msg(_("Memoria insufficiente"), 1); return; }
|
|
*j = *r;
|
|
snprintf(g_label, sizeof g_label, "%s", r->name[0] ? r->name : _("Installazione"));
|
|
snprintf(g_icon, sizeof g_icon, "%s", r->icon);
|
|
snprintf(g_libid, sizeof g_libid, "%s", r->lib_id);
|
|
g_phase[0] = 0; g_done = g_total = 0; g_dl = 0; g_ended = 0;
|
|
g_started = SDL_GetTicks();
|
|
SDL_AtomicSet(&g_state, 1);
|
|
SDL_Thread *t = SDL_CreateThreadWithStackSize(install_thread, "install", 512 * 1024, j);
|
|
if (t) SDL_DetachThread(t);
|
|
else { free(j); SDL_AtomicSet(&g_state, 0); set_msg(_("Impossibile avviare l'installazione"), 1); }
|
|
}
|
|
|
|
// una alla volta, come sulla console: le altre aspettano in coda
|
|
static void install_start(const InstallReq *r) {
|
|
if (SDL_AtomicGet(&g_state) == 1) {
|
|
for (int i = 0; i < g_nq; i++) if (!strcmp(g_queue[i].url, r->url)) { set_msg(_("È già in coda"), 0); return; }
|
|
if (g_nq >= QMAX) { set_msg(_("Troppe installazioni in coda: aspetta che finiscano"), 1); return; }
|
|
g_queue[g_nq++] = *r;
|
|
char m[200]; snprintf(m, sizeof m, _("In coda: %s (%d in attesa)"), r->name[0] ? r->name : _("Installazione"), g_nq);
|
|
set_msg(m, 0);
|
|
return;
|
|
}
|
|
install_run(r);
|
|
}
|
|
|
|
static void lib_removed(int st, JVal *j, const char *raw, void *ud) { (void)j; (void)raw; (void)ud; if (st != 200) omega_log("install: file caricati non tolti (%d)", st); }
|
|
|
|
// dal ciclo principale: chiude l'installazione quando il thread ha finito
|
|
void install_tick(void) {
|
|
if (SDL_AtomicGet(&g_state) != 2) return;
|
|
SDL_AtomicSet(&g_state, 0);
|
|
g_ended = SDL_GetTicks(); snprintf(g_end_name, sizeof g_end_name, "%s", g_label); snprintf(g_end_icon, sizeof g_end_icon, "%s", g_icon);
|
|
set_msg(g_result, g_result_err);
|
|
if (!g_result_err && g_libid[0]) {
|
|
// file caricati dal telefono o dal PC: installati, non servono più (niente orfani)
|
|
char esc[100], body[140]; json_escape(esc, sizeof esc, g_libid); snprintf(body, sizeof body, "{\"id\":\"%s\"}", esc);
|
|
net_req(HTTP_POST, "http://127.0.0.1:9095/v1/library/remove", body, lib_removed, NULL);
|
|
omega_log("install: tolgo i file caricati della voce %s", g_libid);
|
|
}
|
|
if (g_installed_title) {
|
|
toast(IC_DOWNLOAD, NULL, 0, g_label, _("Installato: ora è nella tua Home"));
|
|
scan_apps();
|
|
} else if (!g_result_err) {
|
|
toast(IC_DOWNLOAD, NULL, 0, g_label, g_result);
|
|
} else toast(IC_CLOSE, NULL, 0, g_label, g_result);
|
|
if (g_nq > 0) { InstallReq next = g_queue[0]; memmove(&g_queue[0], &g_queue[1], sizeof(InstallReq) * (size_t)(g_nq - 1)); g_nq--; install_run(&next); }
|
|
}
|
|
|
|
int install_view(InstallView *v) {
|
|
memset(v, 0, sizeof *v);
|
|
v->queued = g_nq;
|
|
if (SDL_AtomicGet(&g_state) == 1) {
|
|
v->active = 1;
|
|
snprintf(v->name, sizeof v->name, "%s", g_label); snprintf(v->icon, sizeof v->icon, "%s", g_icon);
|
|
long done = g_done, total = g_total;
|
|
v->prog = total > 0 && g_dl ? clampf((float)done / (float)total, 0, 1) : -1;
|
|
if (total > 0 && g_dl) {
|
|
int pc = (int)(v->prog * 100);
|
|
if (total > 1048576 * 4 && strcmp(g_phase, _("Scaricamento")) && strcmp(g_phase, _("Copia sulla console"))) snprintf(v->phase, sizeof v->phase, "%s \xC2\xB7 %d%%", g_phase, pc); // stato di sistema in KB
|
|
else snprintf(v->phase, sizeof v->phase, _("%s · %d%% · %.0f / %.0f MB"), g_phase, pc, done / 1048576.0, total / 1048576.0);
|
|
} else snprintf(v->phase, sizeof v->phase, "%s...", g_phase[0] ? g_phase : _("In corso"));
|
|
return 1;
|
|
}
|
|
// finito da poco: resta visibile (gli errori di più, finché non si chiudono)
|
|
if (g_ended && SDL_GetTicks() - g_ended < (g_result_err ? 60000u : 12000u)) {
|
|
v->ended = g_ended; v->err = g_result_err; v->prog = 1;
|
|
snprintf(v->name, sizeof v->name, "%s", g_end_name); snprintf(v->icon, sizeof v->icon, "%s", g_end_icon);
|
|
snprintf(v->result, sizeof v->result, "%s", g_result);
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
void install_dismiss(void) { g_ended = 0; }
|
|
void install_cancel(void) { if (SDL_AtomicGet(&g_state) == 1) g_cancel = 1; g_nq = 0; }
|
|
|
|
// File orfani: scaricamenti a metà o di installazioni finite, icone vecchie e
|
|
// cartelle di giochi esterni la cui registrazione non è andata a buon fine.
|
|
// Si toglie solo ciò che Omega stessa ha creato.
|
|
static void rm_old(const char *dir, time_t older, int depth) {
|
|
DIR *d = opendir(dir); if (!d) return;
|
|
struct dirent *e; time_t now = time(NULL);
|
|
while ((e = readdir(d))) {
|
|
if (e->d_name[0] == '.') continue;
|
|
char p[700]; snprintf(p, sizeof p, "%s/%s", dir, e->d_name);
|
|
struct stat st; if (lstat(p, &st) != 0) continue;
|
|
if (S_ISDIR(st.st_mode)) { if (depth > 0) { rm_old(p, older, depth - 1); rmdir(p); } continue; }
|
|
if (now - st.st_mtime > older) { unlink(p); omega_log("pulizia: tolto %s", p); }
|
|
}
|
|
closedir(d);
|
|
}
|
|
static int cleanup_thread(void *ud) {
|
|
(void)ud;
|
|
SDL_Delay(15000); // dopo l'avvio, senza rubare tempo alla home
|
|
if (install_busy()) return 0;
|
|
rm_old(OMEGA_DIR "/dl", 2 * 3600, 1); // pkg/zip scaricati, .part, icone dei pkg
|
|
#ifdef PS5
|
|
// /user/app/<TID> con il nostro mount.lnk ma senza appmeta: registrazione fallita
|
|
DIR *d = opendir("/user/app");
|
|
struct dirent *e;
|
|
while (d && (e = readdir(d))) {
|
|
if (e->d_name[0] == '.' || strlen(e->d_name) != 9) continue;
|
|
char ln[128], meta[96]; snprintf(ln, sizeof ln, "/user/app/%s/mount.lnk", e->d_name); snprintf(meta, sizeof meta, "/user/appmeta/%s", e->d_name);
|
|
struct stat st;
|
|
if (stat(ln, &st) == 0 && access(meta, 0) != 0 && time(NULL) - st.st_mtime > 3600) {
|
|
char dir[96]; snprintf(dir, sizeof dir, "/user/app/%s", e->d_name);
|
|
omega_log("pulizia: registrazione fallita di %s, tolgo %s", e->d_name, dir);
|
|
copy_tree_rm(dir);
|
|
}
|
|
}
|
|
if (d) closedir(d);
|
|
#endif
|
|
return 0;
|
|
}
|
|
void install_cleanup(void) {
|
|
SDL_Thread *t = SDL_CreateThread(cleanup_thread, "cleanup", NULL);
|
|
if (t) SDL_DetachThread(t);
|
|
}
|
|
|
|
void install_overlay(void) {
|
|
if (SDL_AtomicGet(&g_state) != 1) return;
|
|
if (g_scene == SC_HOME && ov_depth() == 0) return; // in home c'è la tessera nella fila
|
|
int w = 600, h = 132, x = SCREEN_W - w - 40, y = SCREEN_H - h - 110;
|
|
shadow_rrect(x, y, w, h, 24, 22, 150);
|
|
fill_rrect(x, y, w, h, 24, RGB(28, 32, 46), 245);
|
|
draw_icon(IC_DOWNLOAD, x + 60, y + h / 2, 44, C_ACC2, 255);
|
|
draw_text_fit(font(W_MED, 28), g_label, x + 110, y + 24, w - 150, C_TXT, 255, AL_L);
|
|
long done = g_done, total = g_total;
|
|
char sub[256];
|
|
if (total > 0 && done >= 0 && g_dl) {
|
|
snprintf(sub, sizeof sub, _("%s · %.1f / %.1f MB"), g_phase, done / 1048576.0, total / 1048576.0);
|
|
} else {
|
|
Uint32 s = (SDL_GetTicks() - g_started) / 1000;
|
|
snprintf(sub, sizeof sub, "%s... %us", g_phase[0] ? g_phase : _("In corso"), s);
|
|
}
|
|
draw_text_fit(font(W_REG, 22), sub, x + 110, y + 62, w - 150, C_DIM, 255, AL_L);
|
|
int bx = x + 110, bw = w - 150, by = y + h - 30;
|
|
fill_rrect(bx, by, bw, 8, 4, RGB(255, 255, 255), 30);
|
|
if (total > 0 && g_dl) fill_rrect(bx, by, (int)(bw * clampf((float)done / (float)total, 0, 1)), 8, 4, C_ACC2, 230);
|
|
else { int iw = bw / 3, ix = (int)(fmodf((float)g_time * 400, (float)(bw + iw)) - iw); fill_rrect(bx + (ix < 0 ? 0 : ix), by, ix < 0 ? iw + ix : iw, 8, 4, C_ACC2, 230); }
|
|
}
|