mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-18 19:54:21 +02:00
cafa6f3d61
Ships a dual-NRO native launcher that checks GitHub Releases, updates both NROs from gen1recomp-*-switch.zip with matching NACP versions, and stays silent unless an update needs confirm.
293 lines
10 KiB
C
293 lines
10 KiB
C
#include "ota_protocol.h"
|
|
|
|
#include <ctype.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
typedef struct {
|
|
int major;
|
|
int minor;
|
|
int patch;
|
|
int ok;
|
|
} semver_t;
|
|
|
|
static void set_reason(char *buf, size_t n, const char *r) {
|
|
if (!buf || n == 0) return;
|
|
snprintf(buf, n, "%s", r ? r : "");
|
|
}
|
|
|
|
static int parse_semver(const char *s, semver_t *out) {
|
|
memset(out, 0, sizeof(*out));
|
|
if (!s || !*s) return 0;
|
|
if (s[0] == 'v' || s[0] == 'V') s++;
|
|
int maj = 0, min = 0, pat = 0;
|
|
char trail = 0;
|
|
if (sscanf(s, "%d.%d.%d%c", &maj, &min, &pat, &trail) != 3) return 0;
|
|
if (maj < 0 || min < 0 || pat < 0) return 0;
|
|
out->major = maj;
|
|
out->minor = min;
|
|
out->patch = pat;
|
|
out->ok = 1;
|
|
return 1;
|
|
}
|
|
|
|
int ota_compare_semver(const char *a, const char *b) {
|
|
semver_t pa, pb;
|
|
int oa = parse_semver(a, &pa);
|
|
int ob = parse_semver(b, &pb);
|
|
if (!oa && !ob) return 0;
|
|
if (!oa) return -1;
|
|
if (!ob) return 1;
|
|
if (pa.major != pb.major) return pa.major < pb.major ? -1 : 1;
|
|
if (pa.minor != pb.minor) return pa.minor < pb.minor ? -1 : 1;
|
|
if (pa.patch != pb.patch) return pa.patch < pb.patch ? -1 : 1;
|
|
return 0;
|
|
}
|
|
|
|
int ota_is_ota_asset_name(const char *name) {
|
|
int maj = 0, min = 0, pat = 0;
|
|
if (!name) return 0;
|
|
/* Unified SD/OTA asset: gen1recomp-X.Y.Z-switch.zip */
|
|
size_t len = strlen(name);
|
|
const char *suffix = "-switch.zip";
|
|
size_t slen = strlen(suffix);
|
|
if (len <= slen) return 0;
|
|
if (strcmp(name + len - slen, suffix) != 0) return 0;
|
|
if (strncmp(name, "gen1recomp-", 11) != 0) return 0;
|
|
if (sscanf(name + 11, "%d.%d.%d", &maj, &min, &pat) != 3) return 0;
|
|
char rebuilt[128];
|
|
snprintf(rebuilt, sizeof(rebuilt), "gen1recomp-%d.%d.%d-switch.zip", maj, min, pat);
|
|
return strcmp(name, rebuilt) == 0;
|
|
}
|
|
|
|
int ota_version_from_ota_asset(const char *name, char *out, size_t out_len) {
|
|
int maj = 0, min = 0, pat = 0;
|
|
if (!out || out_len == 0) return 0;
|
|
out[0] = '\0';
|
|
if (!ota_is_ota_asset_name(name)) return 0;
|
|
if (sscanf(name + 11, "%d.%d.%d", &maj, &min, &pat) != 3) return 0;
|
|
snprintf(out, out_len, "%d.%d.%d", maj, min, pat);
|
|
return 1;
|
|
}
|
|
|
|
static const char *find_json_string(const char *json, const char *key, char *out, size_t out_len) {
|
|
char pattern[128];
|
|
snprintf(pattern, sizeof(pattern), "\"%s\"", key);
|
|
const char *p = strstr(json, pattern);
|
|
if (!p) return NULL;
|
|
p = strchr(p + strlen(pattern), ':');
|
|
if (!p) return NULL;
|
|
p++;
|
|
while (*p && isspace((unsigned char)*p)) p++;
|
|
if (*p != '"') return NULL;
|
|
p++;
|
|
size_t i = 0;
|
|
while (*p && *p != '"' && i + 1 < out_len) {
|
|
if (*p == '\\' && p[1]) {
|
|
p++;
|
|
out[i++] = *p++;
|
|
continue;
|
|
}
|
|
out[i++] = *p++;
|
|
}
|
|
out[i] = '\0';
|
|
return out;
|
|
}
|
|
|
|
int ota_parse_release(const char *json_text, ota_release_t *out) {
|
|
memset(out, 0, sizeof(*out));
|
|
if (!json_text || !*json_text) {
|
|
set_reason(out->reason, sizeof(out->reason), "empty_json");
|
|
return 0;
|
|
}
|
|
if (!find_json_string(json_text, "tag_name", out->tag, sizeof(out->tag))) {
|
|
set_reason(out->reason, sizeof(out->reason), "missing_tag");
|
|
return 0;
|
|
}
|
|
semver_t sv;
|
|
if (!parse_semver(out->tag, &sv)) {
|
|
set_reason(out->reason, sizeof(out->reason), "bad_tag");
|
|
return 0;
|
|
}
|
|
snprintf(out->version, sizeof(out->version), "%d.%d.%d", sv.major, sv.minor, sv.patch);
|
|
|
|
/* Scan for OTA asset name then its browser_download_url in the same object-ish window. */
|
|
const char *cursor = json_text;
|
|
while ((cursor = strstr(cursor, "\"name\"")) != NULL) {
|
|
char name[128];
|
|
if (!find_json_string(cursor, "name", name, sizeof(name))) {
|
|
cursor += 6;
|
|
continue;
|
|
}
|
|
if (ota_is_ota_asset_name(name)) {
|
|
snprintf(out->asset_name, sizeof(out->asset_name), "%s", name);
|
|
/* Prefer URL after this name key within a limited window. */
|
|
const char *window_end = cursor + 800;
|
|
if (window_end > json_text + strlen(json_text)) window_end = json_text + strlen(json_text);
|
|
char url[512];
|
|
const char *u = NULL;
|
|
const char *scan = cursor;
|
|
while (scan < window_end && (scan = strstr(scan, "\"browser_download_url\"")) != NULL &&
|
|
scan < window_end) {
|
|
if (find_json_string(scan, "browser_download_url", url, sizeof(url))) {
|
|
u = url;
|
|
break;
|
|
}
|
|
scan += 21;
|
|
}
|
|
if (!u || !*u) {
|
|
set_reason(out->reason, sizeof(out->reason), "missing_download_url");
|
|
return 0;
|
|
}
|
|
snprintf(out->download_url, sizeof(out->download_url), "%s", u);
|
|
out->ok = 1;
|
|
return 1;
|
|
}
|
|
cursor += 6;
|
|
}
|
|
set_reason(out->reason, sizeof(out->reason), "missing_ota_asset");
|
|
return 0;
|
|
}
|
|
|
|
void ota_decide_update(const char *installed_version, const ota_release_t *release,
|
|
ota_decision_t *out) {
|
|
memset(out, 0, sizeof(*out));
|
|
semver_t inst;
|
|
if (!parse_semver(installed_version, &inst)) {
|
|
snprintf(out->status, sizeof(out->status), "error");
|
|
set_reason(out->reason, sizeof(out->reason), "bad_installed_version");
|
|
return;
|
|
}
|
|
if (!release || !release->ok) {
|
|
snprintf(out->status, sizeof(out->status), "error");
|
|
set_reason(out->reason, sizeof(out->reason), "bad_release");
|
|
return;
|
|
}
|
|
if (ota_compare_semver(release->version, installed_version) <= 0) {
|
|
snprintf(out->status, sizeof(out->status), "uptodate");
|
|
snprintf(out->version, sizeof(out->version), "%s", installed_version);
|
|
return;
|
|
}
|
|
snprintf(out->status, sizeof(out->status), "available");
|
|
snprintf(out->version, sizeof(out->version), "%s", release->version);
|
|
snprintf(out->asset_name, sizeof(out->asset_name), "%s", release->asset_name);
|
|
snprintf(out->download_url, sizeof(out->download_url), "%s", release->download_url);
|
|
}
|
|
|
|
int ota_lookup_sum(const char *sums_text, const char *asset_name, char *out_hex, size_t out_len) {
|
|
if (out_hex && out_len) out_hex[0] = '\0';
|
|
if (!sums_text || !asset_name || !out_hex || out_len < 65) return 0;
|
|
const char *p = sums_text;
|
|
while (*p) {
|
|
char hex[96];
|
|
char name[256];
|
|
int n = 0;
|
|
if (sscanf(p, "%95s %255s%n", hex, name, &n) >= 2 && n > 0) {
|
|
const char *nm = name;
|
|
if (nm[0] == '*') nm++;
|
|
if (strncmp(nm, "./", 2) == 0) nm += 2;
|
|
if (strcmp(nm, asset_name) == 0) {
|
|
/* normalize hex to lowercase */
|
|
size_t i;
|
|
for (i = 0; hex[i] && i + 1 < out_len; i++)
|
|
out_hex[i] = (char)tolower((unsigned char)hex[i]);
|
|
out_hex[i] = '\0';
|
|
return 1;
|
|
}
|
|
p += n;
|
|
while (*p && *p != '\n') p++;
|
|
if (*p == '\n') p++;
|
|
continue;
|
|
}
|
|
while (*p && *p != '\n') p++;
|
|
if (*p == '\n') p++;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void ota_verify_sha256(const char *asset_name, const char *actual_hex, const char *sums_text,
|
|
ota_verify_t *out) {
|
|
memset(out, 0, sizeof(*out));
|
|
if (!asset_name || !*asset_name) {
|
|
set_reason(out->reason, sizeof(out->reason), "bad_asset_name");
|
|
return;
|
|
}
|
|
if (!sums_text) {
|
|
set_reason(out->reason, sizeof(out->reason), "missing_sums");
|
|
return;
|
|
}
|
|
char expected[96];
|
|
if (!ota_lookup_sum(sums_text, asset_name, expected, sizeof(expected))) {
|
|
set_reason(out->reason, sizeof(out->reason), "sum_not_found");
|
|
return;
|
|
}
|
|
if (!actual_hex || !*actual_hex) {
|
|
set_reason(out->reason, sizeof(out->reason), "missing_actual_hash");
|
|
return;
|
|
}
|
|
char actual[96];
|
|
size_t i;
|
|
for (i = 0; actual_hex[i] && i + 1 < sizeof(actual); i++)
|
|
actual[i] = (char)tolower((unsigned char)actual_hex[i]);
|
|
actual[i] = '\0';
|
|
if (strcmp(actual, expected) != 0) {
|
|
set_reason(out->reason, sizeof(out->reason), "hash_mismatch");
|
|
return;
|
|
}
|
|
out->ok = 1;
|
|
}
|
|
|
|
void ota_plan_atomic_apply(const char *install_dir, const char *verified_temp,
|
|
ota_apply_plan_t *out) {
|
|
memset(out, 0, sizeof(*out));
|
|
if (!install_dir || !*install_dir) install_dir = OTA_INSTALL_DIR;
|
|
if (!verified_temp) verified_temp = "";
|
|
snprintf(out->game_nro, sizeof(out->game_nro), "%s/%s", install_dir, OTA_GAME_NRO_NAME);
|
|
snprintf(out->part_path, sizeof(out->part_path), "%s.part", out->game_nro);
|
|
snprintf(out->launcher_nro, sizeof(out->launcher_nro), "%s/%s", install_dir,
|
|
OTA_LAUNCHER_NRO_NAME);
|
|
snprintf(out->launcher_part, sizeof(out->launcher_part), "%s.part", out->launcher_nro);
|
|
snprintf(out->next_load, sizeof(out->next_load), "%s", out->game_nro);
|
|
/* verified_temp is the game NRO; launcher replace uses a sibling verified path */
|
|
snprintf(out->steps[0], sizeof(out->steps[0]), "copy_to_part:%s->%s", verified_temp,
|
|
out->part_path);
|
|
snprintf(out->steps[1], sizeof(out->steps[1]), "rename:%s->%s", out->part_path, out->game_nro);
|
|
snprintf(out->steps[2], sizeof(out->steps[2]), "copy_to_part:launcher->%s", out->launcher_part);
|
|
snprintf(out->steps[3], sizeof(out->steps[3]), "rename:%s->%s", out->launcher_part,
|
|
out->launcher_nro);
|
|
snprintf(out->steps[4], sizeof(out->steps[4]), "env_set_next_load:%s", out->game_nro);
|
|
snprintf(out->preserve, sizeof(out->preserve), "%s/%s", install_dir, OTA_SAVE_DIR_NAME);
|
|
snprintf(out->forbidden_delete, sizeof(out->forbidden_delete), "delete:%s/%s", install_dir,
|
|
OTA_SAVE_DIR_NAME);
|
|
snprintf(out->forbidden_direct, sizeof(out->forbidden_direct), "write_direct:%s", out->game_nro);
|
|
}
|
|
|
|
void ota_offline_policy(double elapsed_sec, const ota_offline_events_t *events, ota_offline_t *out) {
|
|
memset(out, 0, sizeof(*out));
|
|
ota_offline_events_t ev = {0, -1, 0};
|
|
if (events) ev = *events;
|
|
if (ev.user_skip) {
|
|
snprintf(out->action, sizeof(out->action), "play_installed");
|
|
set_reason(out->reason, sizeof(out->reason), "user_skip");
|
|
snprintf(out->message, sizeof(out->message), "update skipped");
|
|
return;
|
|
}
|
|
if (ev.api_error || ev.network_ok == 0) {
|
|
snprintf(out->action, sizeof(out->action), "play_installed");
|
|
set_reason(out->reason, sizeof(out->reason), "offline_or_error");
|
|
snprintf(out->message, sizeof(out->message),
|
|
"offline or update check failed — play installed version");
|
|
return;
|
|
}
|
|
if (elapsed_sec >= (double)OTA_CHECK_TIMEOUT_SEC) {
|
|
snprintf(out->action, sizeof(out->action), "play_installed");
|
|
set_reason(out->reason, sizeof(out->reason), "timeout");
|
|
snprintf(out->message, sizeof(out->message), "update check timed out after %ds",
|
|
OTA_CHECK_TIMEOUT_SEC);
|
|
return;
|
|
}
|
|
snprintf(out->action, sizeof(out->action), "keep_checking");
|
|
set_reason(out->reason, sizeof(out->reason), "in_flight");
|
|
}
|