Files

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); }
}