// 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 #include #include #include #include #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, 16) < 5 || strcmp(f[0], "MBMNETSHOP") != 0) return false; *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, 16) < 13) 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]); a->description = strdup(f[12]); } 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/list", 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); }