485 lines
16 KiB
C
485 lines
16 KiB
C
// Katalog, Symbole und installierte Apps des MBMNET Shops.
|
|
//
|
|
// Der Katalog kommt vom MBMNET-Server (/shop/list, Quelle Universal-DB) und wird auf der SD
|
|
// zwischengespeichert, damit der Shop auch ohne Verbindung die installierten Apps zeigt.
|
|
// Was der Shop installiert hat, steht in SHOP_DIR/installed.txt (nur dann kann er es wieder entfernen).
|
|
#include <3ds.h>
|
|
#include <dirent.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
|
|
#include "net.h"
|
|
#include "store.h"
|
|
|
|
#define LIST_FILE SHOP_DIR "/list.txt"
|
|
#define INSTALLED_FILE SHOP_DIR "/installed.txt"
|
|
#define MAX_SHEETS 8
|
|
|
|
// --- Katalog ---------------------------------------------------------------------
|
|
|
|
static volatile CatalogState catState = CAT_IDLE;
|
|
static char catMsg[128];
|
|
static bool offline;
|
|
static App *apps;
|
|
static int appCount;
|
|
static int sheetCount;
|
|
static char sheetPath[MAX_SHEETS][64];
|
|
static char *extraInstalled; // Einträge in installed.txt zu Apps, die es im Katalog nicht (mehr) gibt
|
|
static Thread catThread, jobThread;
|
|
|
|
CatalogState catalog_state(void) { return catState; }
|
|
const char *catalog_message(void) { return catMsg; }
|
|
bool catalog_offline(void) { return offline; }
|
|
int catalog_count(void) { return catState == CAT_READY ? appCount : 0; }
|
|
App *catalog_app(int i) { return &apps[i]; }
|
|
int catalog_sheet_count(void) { return sheetCount; }
|
|
const char *catalog_sheet_path(int n) { return n >= 0 && n < sheetCount ? sheetPath[n] : ""; }
|
|
|
|
AppStatus app_status(const App *a)
|
|
{
|
|
if (!a->installed) return APP_NEW;
|
|
if (a->version[0] && strcmp(a->version, a->installedVersion) != 0) return APP_UPDATE;
|
|
return APP_INSTALLED;
|
|
}
|
|
|
|
static bool file_exists(const char *path)
|
|
{
|
|
struct stat st;
|
|
return stat(path, &st) == 0;
|
|
}
|
|
|
|
static bool write_file(const char *path, const void *data, u32 size)
|
|
{
|
|
char tmp[96];
|
|
snprintf(tmp, sizeof(tmp), "%s.neu", path);
|
|
FILE *f = fopen(tmp, "wb");
|
|
bool ok = f && fwrite(data, 1, size, f) == size;
|
|
if (f) fclose(f);
|
|
if (ok) { remove(path); ok = rename(tmp, path) == 0; }
|
|
if (!ok) remove(tmp);
|
|
return ok;
|
|
}
|
|
|
|
static char *read_file(const char *path, u32 *size)
|
|
{
|
|
FILE *f = fopen(path, "rb");
|
|
if (!f) return NULL;
|
|
fseek(f, 0, SEEK_END);
|
|
long n = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
char *buf = n >= 0 ? malloc(n + 1) : NULL;
|
|
if (buf && fread(buf, 1, n, f) != (size_t)n) { free(buf); buf = NULL; }
|
|
fclose(f);
|
|
if (buf) { buf[n] = 0; if (size) *size = n; }
|
|
return buf;
|
|
}
|
|
|
|
// Zerlegt eine Zeile in Felder (Tab-getrennt). Gibt die Anzahl zurück.
|
|
static int split(char *line, char **fields, int max)
|
|
{
|
|
int n = 0;
|
|
while (n < max) {
|
|
fields[n++] = line;
|
|
char *tab = strchr(line, '\t');
|
|
if (!tab) break;
|
|
*tab = 0;
|
|
line = tab + 1;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
#define COPY(dst, src) snprintf(dst, sizeof(dst), "%s", src)
|
|
|
|
static bool parse_list(char *text, int *revision)
|
|
{
|
|
char *save = NULL, *line = strtok_r(text, "\n", &save);
|
|
char *f[16];
|
|
if (!line || split(line, f, 20) < 5 || strcmp(f[0], "MBMNETSHOP") != 0) return false;
|
|
int format = atoi(f[1]);
|
|
int fields = format >= 2 ? 17 : 13;
|
|
*revision = atoi(f[2]);
|
|
int count = atoi(f[3]);
|
|
sheetCount = atoi(f[4]);
|
|
if (sheetCount > MAX_SHEETS) sheetCount = MAX_SHEETS;
|
|
if (count <= 0 || count > 5000) return false;
|
|
apps = calloc(count, sizeof(App));
|
|
if (!apps) return false;
|
|
|
|
appCount = 0;
|
|
while ((line = strtok_r(NULL, "\n", &save)) && appCount < count) {
|
|
if (split(line, f, 20) < fields) continue;
|
|
App *a = &apps[appCount++];
|
|
COPY(a->slug, f[0]);
|
|
COPY(a->title, f[1]);
|
|
COPY(a->author, f[2]);
|
|
COPY(a->version, f[3]);
|
|
COPY(a->category, f[4]);
|
|
a->sheet = atoi(f[5]);
|
|
a->icon = atoi(f[6]);
|
|
COPY(a->kind, f[7]);
|
|
COPY(a->file, f[8]);
|
|
a->size = strtoul(f[9], NULL, 10);
|
|
a->stars = strtoul(f[10], NULL, 10);
|
|
COPY(a->updated, f[11]);
|
|
if (format >= 2) {
|
|
a->shots = atoi(f[12]);
|
|
a->banner = f[13][0] == '1';
|
|
a->colorBg = f[14][0] == '#' ? strtoul(f[14] + 1, NULL, 16) : 0;
|
|
COPY(a->license, f[15]);
|
|
}
|
|
a->description = strdup(f[fields - 1]);
|
|
}
|
|
return appCount > 0;
|
|
}
|
|
|
|
static App *find_app(const char *slug)
|
|
{
|
|
for (int i = 0; i < appCount; i++)
|
|
if (strcmp(apps[i].slug, slug) == 0) return &apps[i];
|
|
return NULL;
|
|
}
|
|
|
|
// installed.txt: slug \t Version \t Art \t Titel-ID (hex) \t Dateiname
|
|
static void load_installed(void)
|
|
{
|
|
char *text = read_file(INSTALLED_FILE, NULL);
|
|
if (!text) return;
|
|
size_t extraLen = 0;
|
|
char *save = NULL, *line, *f[8];
|
|
for (line = strtok_r(text, "\n", &save); line; line = strtok_r(NULL, "\n", &save)) {
|
|
char copy[256];
|
|
COPY(copy, line);
|
|
if (split(line, f, 8) < 5) continue;
|
|
App *a = find_app(f[0]);
|
|
if (!a) {
|
|
// unbekannte App merken, damit sie beim Speichern nicht verloren geht
|
|
size_t len = strlen(copy);
|
|
char *bigger = realloc(extraInstalled, extraLen + len + 2);
|
|
if (!bigger) continue;
|
|
extraInstalled = bigger;
|
|
memcpy(extraInstalled + extraLen, copy, len);
|
|
extraLen += len;
|
|
extraInstalled[extraLen++] = '\n';
|
|
extraInstalled[extraLen] = 0;
|
|
continue;
|
|
}
|
|
a->installed = true;
|
|
COPY(a->installedVersion, f[1]);
|
|
a->titleId = strtoull(f[3], NULL, 16);
|
|
if (f[4][0]) COPY(a->file, f[4]);
|
|
}
|
|
free(text);
|
|
}
|
|
|
|
static void save_installed(void)
|
|
{
|
|
FILE *f = fopen(INSTALLED_FILE, "w");
|
|
if (!f) return;
|
|
for (int i = 0; i < appCount; i++) {
|
|
App *a = &apps[i];
|
|
if (a->installed)
|
|
fprintf(f, "%s\t%s\t%s\t%016llX\t%s\n", a->slug, a->installedVersion, a->kind, a->titleId, a->file);
|
|
}
|
|
if (extraInstalled) fputs(extraInstalled, f);
|
|
fclose(f);
|
|
}
|
|
|
|
// Alte Symbol-Blätter (andere Revision) löschen
|
|
static void remove_old_sheets(int revision)
|
|
{
|
|
char keep[32];
|
|
snprintf(keep, sizeof(keep), "icons-%d-", revision);
|
|
DIR *d = opendir(SHOP_DIR);
|
|
if (!d) return;
|
|
struct dirent *e;
|
|
char path[320];
|
|
while ((e = readdir(d))) {
|
|
if (strncmp(e->d_name, "icons-", 6) == 0 && strncmp(e->d_name, keep, strlen(keep)) != 0) {
|
|
snprintf(path, sizeof(path), SHOP_DIR "/%s", e->d_name);
|
|
remove(path);
|
|
}
|
|
}
|
|
closedir(d);
|
|
}
|
|
|
|
static void catalog_worker(void *arg)
|
|
{
|
|
(void)arg;
|
|
char url[160];
|
|
u8 *data = NULL;
|
|
u32 size = 0;
|
|
Result rc;
|
|
mkdir("sdmc:/3ds", 0777);
|
|
mkdir(SHOP_DIR, 0777);
|
|
|
|
snprintf(catMsg, sizeof(catMsg), "Lade den Katalog ...");
|
|
snprintf(url, sizeof(url), "http://%s/shop/list2", SHOP_SERVER);
|
|
u32 status = net_get(url, &data, &size, 4 * 1024 * 1024, &rc);
|
|
char *text = NULL;
|
|
if (status == 200 && size > 10) {
|
|
write_file(LIST_FILE, data, size);
|
|
text = realloc(data, size + 1);
|
|
if (text) text[size] = 0; else free(data);
|
|
} else {
|
|
free(data);
|
|
text = read_file(LIST_FILE, &size);
|
|
offline = true;
|
|
if (!text) {
|
|
if (status) snprintf(catMsg, sizeof(catMsg), "Der Shop-Server antwortet nicht (HTTP %lu).", (unsigned long)status);
|
|
else snprintf(catMsg, sizeof(catMsg), "Keine Verbindung zum MBMNET-Server (%08lX).\nIst das WLAN an?", (unsigned long)rc);
|
|
catState = CAT_ERROR;
|
|
return;
|
|
}
|
|
}
|
|
int revision = 0;
|
|
bool ok = parse_list(text, &revision);
|
|
free(text);
|
|
if (!ok) {
|
|
snprintf(catMsg, sizeof(catMsg), "Der Katalog ist beschädigt.");
|
|
catState = CAT_ERROR;
|
|
return;
|
|
}
|
|
load_installed();
|
|
|
|
// Symbol-Blätter (einmal pro Katalog-Revision laden)
|
|
for (int n = 0; n < sheetCount; n++) {
|
|
snprintf(sheetPath[n], sizeof(sheetPath[n]), SHOP_DIR "/icons-%d-%d.t3x", revision, n);
|
|
if (file_exists(sheetPath[n])) continue;
|
|
if (offline) { sheetPath[n][0] = 0; continue; }
|
|
snprintf(catMsg, sizeof(catMsg), "Lade die Symbole (%d von %d) ...", n + 1, sheetCount);
|
|
snprintf(url, sizeof(url), "http://%s/shop/sheet/%d", SHOP_SERVER, n);
|
|
if (net_get(url, &data, &size, 16 * 1024 * 1024, &rc) != 200 || !write_file(sheetPath[n], data, size))
|
|
sheetPath[n][0] = 0;
|
|
free(data);
|
|
}
|
|
if (!offline) remove_old_sheets(revision);
|
|
catMsg[0] = 0;
|
|
catState = CAT_READY;
|
|
}
|
|
|
|
static void start_thread(Thread *t, ThreadFunc fn)
|
|
{
|
|
if (*t) { threadJoin(*t, U64_MAX); threadFree(*t); *t = NULL; }
|
|
s32 prio = 0x30;
|
|
svcGetThreadPriority(&prio, CUR_THREAD_HANDLE);
|
|
*t = threadCreate(fn, NULL, 32 * 1024, prio + 1, -2, false);
|
|
}
|
|
|
|
void catalog_start(void)
|
|
{
|
|
if (catState == CAT_LOADING || catState == CAT_READY) return;
|
|
catState = CAT_LOADING;
|
|
start_thread(&catThread, catalog_worker);
|
|
if (!catThread) { snprintf(catMsg, sizeof(catMsg), "Konnte nicht starten."); catState = CAT_ERROR; }
|
|
}
|
|
|
|
// --- Installieren / Entfernen ------------------------------------------------------
|
|
|
|
static volatile JobState jobState = JOB_IDLE;
|
|
static volatile u32 jobDone, jobTotal;
|
|
static char jobMsg[160];
|
|
static App *jobApp;
|
|
static bool jobRemove;
|
|
|
|
JobState job_state(void) { return jobState; }
|
|
float job_progress(void) { return jobTotal ? (float)jobDone / jobTotal : -1.0f; }
|
|
const char *job_message(void) { return jobMsg; }
|
|
void job_acknowledge(void) { if (jobState != JOB_RUNNING) jobState = JOB_IDLE; }
|
|
|
|
static inline u32 le32(const u8 *p) { return p[0] | p[1] << 8 | p[2] << 16 | (u32)p[3] << 24; }
|
|
static inline u32 be32(const u8 *p) { return (u32)p[0] << 24 | p[1] << 16 | p[2] << 8 | p[3]; }
|
|
static inline u32 align64(u32 x) { return (x + 63) & ~63u; }
|
|
|
|
// Titel-ID aus dem Kopf einer CIA (Kopf, Zertifikate, Ticket, dann die TMD mit der Titel-ID)
|
|
static u64 cia_title_id(const u8 *d, u32 n)
|
|
{
|
|
if (n < 0x20) return 0;
|
|
u32 off = align64(le32(d)) + align64(le32(d + 8)) + align64(le32(d + 12));
|
|
if (off + 4 > n) return 0;
|
|
u32 sigType = be32(d + off), sig;
|
|
switch (sigType) {
|
|
case 0x10003: sig = 0x200 + 0x3C; break;
|
|
case 0x10004: sig = 0x100 + 0x3C; break;
|
|
case 0x10005: sig = 0x3C + 0x40; break;
|
|
default: return 0;
|
|
}
|
|
u32 hdr = off + 4 + sig;
|
|
if (hdr + 0x54 > n) return 0;
|
|
return (u64)be32(d + hdr + 0x4C) << 32 | be32(d + hdr + 0x50);
|
|
}
|
|
|
|
#define HEAD_SIZE (64 * 1024)
|
|
|
|
typedef struct {
|
|
bool cia, started;
|
|
Handle cia_h;
|
|
FILE *file;
|
|
u32 offset;
|
|
u8 *head; // Anfang der Datei (für Prüfung und Titel-ID)
|
|
u32 headLen;
|
|
Result rc;
|
|
const char *error;
|
|
} Install;
|
|
|
|
static bool write_part(Install *in, const u8 *data, u32 len)
|
|
{
|
|
if (!len) return true;
|
|
if (in->cia) {
|
|
u32 w = 0;
|
|
in->rc = FSFILE_Write(in->cia_h, &w, in->offset, data, len, 0);
|
|
if (R_FAILED(in->rc) || w != len) { in->error = "Schreiben fehlgeschlagen"; return false; }
|
|
} else if (fwrite(data, 1, len, in->file) != len) {
|
|
in->error = "SD-Karte voll?";
|
|
return false;
|
|
}
|
|
in->offset += len;
|
|
jobDone = in->offset;
|
|
return true;
|
|
}
|
|
|
|
static bool install_sink(void *p, const u8 *data, u32 len, u32 total)
|
|
{
|
|
Install *in = p;
|
|
jobTotal = total;
|
|
// Den Anfang der Datei mitschreiben (Prüfung, Titel-ID)
|
|
u32 copied = 0;
|
|
if (in->headLen < HEAD_SIZE && len) {
|
|
copied = len < HEAD_SIZE - in->headLen ? len : HEAD_SIZE - in->headLen;
|
|
memcpy(in->head + in->headLen, data, copied);
|
|
in->headLen += copied;
|
|
}
|
|
if (!in->started) {
|
|
if (in->headLen < 8) return true;
|
|
if (in->cia ? le32(in->head) != 0x2020 : memcmp(in->head, "3DSX", 4) != 0) {
|
|
in->error = "Die Datei ist keine gültige App.";
|
|
return false;
|
|
}
|
|
if (in->cia) {
|
|
in->rc = AM_StartCiaInstall(MEDIATYPE_SD, &in->cia_h);
|
|
if (R_FAILED(in->rc)) { in->error = "Installation nicht möglich"; return false; }
|
|
} else if (!(in->file = fopen(SHOP_DIR "/download.tmp", "wb"))) {
|
|
in->error = "Kann nicht auf die SD-Karte schreiben.";
|
|
return false;
|
|
}
|
|
in->started = true;
|
|
// Alles bisher Gesammelte schreiben, vom aktuellen Stück dann nur noch den Rest
|
|
if (!write_part(in, in->head, in->headLen)) return false;
|
|
data += copied;
|
|
len -= copied;
|
|
}
|
|
return write_part(in, data, len);
|
|
}
|
|
|
|
static void install_worker(App *a)
|
|
{
|
|
char url[192];
|
|
Install in = { .cia = strcmp(a->kind, "cia") == 0 };
|
|
in.head = malloc(HEAD_SIZE);
|
|
if (!in.head) { snprintf(jobMsg, sizeof(jobMsg), "Zu wenig Speicher."); jobState = JOB_FAILED; return; }
|
|
snprintf(url, sizeof(url), "http://%s/shop/get/%s", SHOP_SERVER, a->slug);
|
|
Result rc = 0;
|
|
u32 status = net_stream(url, install_sink, &in, &rc);
|
|
if (status == 200 && !in.started && !in.error) in.error = "Die Datei ist keine gültige App.";
|
|
bool ok = status == 200 && in.started && !in.error;
|
|
|
|
if (in.cia) {
|
|
if (in.started) {
|
|
if (ok) {
|
|
in.rc = AM_FinishCiaInstall(in.cia_h);
|
|
if (R_FAILED(in.rc)) { ok = false; in.error = "Installation abgelehnt"; }
|
|
} else {
|
|
AM_CancelCIAInstall(in.cia_h);
|
|
}
|
|
}
|
|
} else if (in.file) {
|
|
fclose(in.file);
|
|
char dest[128];
|
|
snprintf(dest, sizeof(dest), "sdmc:/3ds/%s", a->file);
|
|
if (ok) { remove(dest); ok = rename(SHOP_DIR "/download.tmp", dest) == 0; if (!ok) in.error = "Umbenennen fehlgeschlagen"; }
|
|
else remove(SHOP_DIR "/download.tmp");
|
|
}
|
|
|
|
if (ok) {
|
|
bool update = a->installed;
|
|
a->installed = true;
|
|
COPY(a->installedVersion, a->version);
|
|
if (in.cia) a->titleId = cia_title_id(in.head, in.headLen);
|
|
save_installed();
|
|
snprintf(jobMsg, sizeof(jobMsg), in.cia ? "%s %s!\nDu findest die App jetzt im HOME-Menü."
|
|
: "%s %s!\nStarte sie über den Homebrew Launcher (/3ds/%s).",
|
|
a->title, update ? "aktualisiert" : "installiert", a->file);
|
|
jobState = JOB_DONE;
|
|
} else {
|
|
if (in.error && in.rc) snprintf(jobMsg, sizeof(jobMsg), "%s nicht installiert:\n%s (%08lX).", a->title, in.error, (unsigned long)in.rc);
|
|
else if (in.error) snprintf(jobMsg, sizeof(jobMsg), "%s nicht installiert:\n%s", a->title, in.error);
|
|
else if (status && status != 200) snprintf(jobMsg, sizeof(jobMsg), "%s nicht installiert:\nDer Server meldet HTTP %lu.", a->title, (unsigned long)status);
|
|
else snprintf(jobMsg, sizeof(jobMsg), "%s nicht installiert:\nDownload abgebrochen (%08lX).", a->title, (unsigned long)rc);
|
|
jobState = JOB_FAILED;
|
|
}
|
|
free(in.head);
|
|
}
|
|
|
|
static void remove_worker(App *a)
|
|
{
|
|
bool ok = true;
|
|
Result rc = 0;
|
|
if (strcmp(a->kind, "cia") == 0) {
|
|
if (a->titleId) {
|
|
rc = AM_DeleteTitle(MEDIATYPE_SD, a->titleId);
|
|
// schon von Hand gelöscht? Dann ist es auch gut.
|
|
AM_TitleEntry info;
|
|
u64 id = a->titleId;
|
|
if (R_FAILED(rc) && R_FAILED(AM_GetTitleInfo(MEDIATYPE_SD, 1, &id, &info))) rc = 0;
|
|
ok = R_SUCCEEDED(rc);
|
|
}
|
|
} else {
|
|
char path[128];
|
|
snprintf(path, sizeof(path), "sdmc:/3ds/%s", a->file);
|
|
remove(path);
|
|
}
|
|
if (ok) {
|
|
a->installed = false;
|
|
a->installedVersion[0] = 0;
|
|
a->titleId = 0;
|
|
save_installed();
|
|
snprintf(jobMsg, sizeof(jobMsg), "%s entfernt.", a->title);
|
|
jobState = JOB_DONE;
|
|
} else {
|
|
snprintf(jobMsg, sizeof(jobMsg), "%s ließ sich nicht entfernen (%08lX).", a->title, (unsigned long)rc);
|
|
jobState = JOB_FAILED;
|
|
}
|
|
}
|
|
|
|
static void job_worker(void *arg)
|
|
{
|
|
(void)arg;
|
|
if (jobRemove) remove_worker(jobApp);
|
|
else install_worker(jobApp);
|
|
}
|
|
|
|
static void job_start(App *a, bool remove)
|
|
{
|
|
if (jobState == JOB_RUNNING) return;
|
|
jobApp = a;
|
|
jobRemove = remove;
|
|
jobDone = jobTotal = 0;
|
|
jobMsg[0] = 0;
|
|
jobState = JOB_RUNNING;
|
|
start_thread(&jobThread, job_worker);
|
|
if (!jobThread) { snprintf(jobMsg, sizeof(jobMsg), "Konnte nicht starten."); jobState = JOB_FAILED; }
|
|
}
|
|
|
|
void job_install(App *a) { job_start(a, false); }
|
|
void job_remove(App *a) { job_start(a, true); }
|
|
|
|
void store_exit(void)
|
|
{
|
|
if (catThread) { threadJoin(catThread, U64_MAX); threadFree(catThread); }
|
|
if (jobThread) { threadJoin(jobThread, U64_MAX); threadFree(jobThread); }
|
|
for (int i = 0; i < appCount; i++) free(apps[i].description);
|
|
free(apps);
|
|
free(extraInstalled);
|
|
}
|