Files
MBMNET-APP/shop/source/store.c
T

539 lines
18 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 COPY(dst, src) snprintf(dst, sizeof(dst), "%s", src)
#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] : ""; }
// Bevorzugte Variante, wenn es beide gibt (zuletzt gewählt, Standard CIA)
#define KIND_FILE SHOP_DIR "/format.txt"
static char preferredKind[6] = "cia";
static void load_preferred_kind(void)
{
FILE *f = fopen(KIND_FILE, "r");
if (!f) return;
char k[8] = "";
if (fgets(k, sizeof(k), f) && !strncmp(k, "3dsx", 4)) strcpy(preferredKind, "3dsx");
fclose(f);
}
bool app_has_both(const App *a) { return a->ciaFile[0] && a->dsxFile[0]; }
void app_choose(App *a, const char *kind, bool remember)
{
bool cia = !strcmp(kind, "cia");
if (cia ? !a->ciaFile[0] : !a->dsxFile[0]) return;
COPY(a->kind, cia ? "cia" : "3dsx");
COPY(a->file, cia ? a->ciaFile : a->dsxFile);
a->size = cia ? a->ciaSize : a->dsxSize;
if (remember) {
COPY(preferredKind, a->kind);
FILE *f = fopen(KIND_FILE, "w");
if (f) { fputs(preferredKind, f); fclose(f); }
}
}
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;
}
static bool parse_list(char *text, int *revision)
{
char *save = NULL, *line = strtok_r(text, "\n", &save);
char *f[24];
if (!line || split(line, f, 24) < 5 || strcmp(f[0], "MBMNETSHOP") != 0) return false;
int format = atoi(f[1]);
int fields = format >= 3 ? 21 : 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, 24) < 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]);
}
if (format >= 3) {
COPY(a->ciaFile, f[16]);
a->ciaSize = strtoul(f[17], NULL, 10);
COPY(a->dsxFile, f[18]);
a->dsxSize = strtoul(f[19], NULL, 10);
} else if (!strcmp(a->kind, "cia")) {
COPY(a->ciaFile, a->file);
a->ciaSize = a->size;
} else {
COPY(a->dsxFile, a->file);
a->dsxSize = a->size;
}
a->description = strdup(f[fields - 1]);
if (app_has_both(a)) app_choose(a, preferredKind, false);
}
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]);
COPY(a->instKind, f[2]);
a->titleId = strtoull(f[3], NULL, 16);
COPY(a->instFile, f[4]);
app_choose(a, a->instKind, false); // die Seite zeigt die installierte Variante
}
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->instKind, a->titleId, a->instFile);
}
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/list3", SHOP_SERVER);
load_preferred_kind();
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);
}
// Installierte Variante entfernen (CIA über AM, .3dsx von der SD). Schon von Hand gelöscht zählt als Erfolg.
static Result remove_files(const App *a)
{
if (strcmp(a->instKind, "cia") == 0) {
if (!a->titleId) return 0;
Result rc = AM_DeleteTitle(MEDIATYPE_SD, a->titleId);
AM_TitleEntry info;
u64 id = a->titleId;
if (R_FAILED(rc) && R_FAILED(AM_GetTitleInfo(MEDIATYPE_SD, 1, &id, &info))) rc = 0;
return rc;
}
if (a->instFile[0]) {
char path[128];
snprintf(path, sizeof(path), "sdmc:/3ds/%s", a->instFile);
remove(path);
}
return 0;
}
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/%s", SHOP_SERVER, a->slug, a->kind);
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;
// Vorher die andere Variante installiert? Die alte wegräumen (nur eine von beiden behalten)
if (a->installed && strcmp(a->instKind, a->kind) != 0) remove_files(a);
a->installed = true;
COPY(a->installedVersion, a->version);
COPY(a->instKind, a->kind);
COPY(a->instFile, a->file);
a->titleId = in.cia ? cia_title_id(in.head, in.headLen) : 0;
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)
{
Result rc = remove_files(a);
bool ok = R_SUCCEEDED(rc);
if (ok) {
a->installed = false;
a->installedVersion[0] = a->instKind[0] = a->instFile[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);
}