Add Switch OTA launcher with unified SD zip and quiet UI.

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.
This commit is contained in:
Andrew Quenehen
2026-08-05 01:48:39 -03:00
parent f0ed2efe07
commit cafa6f3d61
25 changed files with 2505 additions and 26 deletions
+165
View File
@@ -0,0 +1,165 @@
#---------------------------------------------------------------------------------
# gen1recomp Switch native OTA launcher
# Requires: DEVKITPRO + switch-curl, switch-mbedtls, switch-zlib, switch-zziplib
# Host protocol tests: make host-test (no DEVKITPRO needed)
#---------------------------------------------------------------------------------
.SUFFIXES:
#---------------------------------------------------------------------------------
# Host-only path when DEVKITPRO is unset
#---------------------------------------------------------------------------------
ifeq ($(strip $(DEVKITPRO)),)
.PHONY: host-test clean-host
host-test:
@mkdir -p build-host
cc -std=c11 -Wall -Wextra -Iinclude -o build-host/test_ota_protocol \
src/ota_protocol.c host/test_ota_protocol.c
./build-host/test_ota_protocol
clean-host:
rm -rf build-host
%:
@echo "DEVKITPRO not set — only 'make host-test' is available."
@echo "Run: bash scripts/switch/install_devkitpro_deps.sh"
@false
else
#---------------------------------------------------------------------------------
TOPDIR ?= $(CURDIR)
include $(DEVKITPRO)/libnx/switch_rules
#---------------------------------------------------------------------------------
TARGET := gen1recomp
BUILD := build
SOURCES := src
DATA := data
INCLUDES := include
APP_TITLE := gen1recomp
APP_AUTHOR := bryanthaboi, port by andrewqsantos
# Overridable: scripts/switch/build_ota_launcher.sh passes release X.Y.Z
APP_VERSION ?= 0.0.0
# Prefer project Switch icon if present
ICON := $(TOPDIR)/../../assets/switch/icon.jpg
#---------------------------------------------------------------------------------
ARCH := -march=armv8-a+crc+crypto -mtune=cortex-a57 -mtp=soft -fPIE
CFLAGS := -g -Wall -O2 -ffunction-sections $(ARCH) $(DEFINES)
CFLAGS += $(INCLUDE) -D__SWITCH__
CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions
ASFLAGS := -g $(ARCH)
LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map)
LIBS := -lcurl -lzzip -lmbedtls -lmbedx509 -lmbedcrypto -lz -lnx
LIBDIRS := $(PORTLIBS) $(LIBNX)
#---------------------------------------------------------------------------------
ifneq ($(BUILD),$(notdir $(CURDIR)))
#---------------------------------------------------------------------------------
export OUTPUT := $(CURDIR)/$(TARGET)
export TOPDIR := $(CURDIR)
export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \
$(foreach dir,$(DATA),$(CURDIR)/$(dir))
export DEPSDIR := $(CURDIR)/$(BUILD)
CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c)))
CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp)))
SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s)))
BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*)))
#---------------------------------------------------------------------------------
# use CXX for linking C++ projects, CC for standard C
#---------------------------------------------------------------------------------
ifeq ($(strip $(CPPFILES)),)
export LD := $(CC)
else
export LD := $(CXX)
endif
#---------------------------------------------------------------------------------
export OFILES_BIN := $(addsuffix .o,$(BINFILES))
export OFILES_SRC := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o)
export OFILES := $(OFILES_BIN) $(OFILES_SRC)
export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \
$(foreach dir,$(LIBDIRS),-I$(dir)/include) \
-I$(CURDIR)/$(BUILD)
export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib)
ifeq ($(strip $(ICON)),)
icons := $(wildcard *.jpg)
ifneq (,$(findstring $(TARGET).jpg,$(icons)))
export APP_ICON := $(TOPDIR)/$(TARGET).jpg
else
ifneq (,$(findstring icon.jpg,$(icons)))
export APP_ICON := $(TOPDIR)/icon.jpg
endif
endif
else
ifeq ($(wildcard $(ICON)),)
# fall back to libnx default if project icon missing
export APP_ICON := $(LIBNX)/default_icon.jpg
else
export APP_ICON := $(ICON)
endif
endif
ifeq ($(strip $(NO_ICON)),)
export NROFLAGS += --icon=$(APP_ICON)
endif
ifeq ($(strip $(NO_NACP)),)
export NROFLAGS += --nacp=$(CURDIR)/$(TARGET).nacp
endif
.PHONY: $(BUILD) clean all host-test
#---------------------------------------------------------------------------------
all: $(BUILD)
$(BUILD):
@[ -d $@ ] || mkdir -p $@
@$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile
#---------------------------------------------------------------------------------
clean:
@echo clean ...
@rm -fr $(BUILD) $(TARGET).nro $(TARGET).nacp $(TARGET).elf $(TARGET).lst build-host
host-test:
@mkdir -p build-host
cc -std=c11 -Wall -Wextra -Iinclude -o build-host/test_ota_protocol \
src/ota_protocol.c host/test_ota_protocol.c
./build-host/test_ota_protocol
#---------------------------------------------------------------------------------
else
.PHONY: all
DEPENDS := $(OFILES:.o=.d)
#---------------------------------------------------------------------------------
all: $(OUTPUT).nro
$(OUTPUT).nro: $(OUTPUT).elf $(OUTPUT).nacp
$(OUTPUT).elf: $(OFILES)
-include $(DEPENDS)
#---------------------------------------------------------------------------------
endif
#---------------------------------------------------------------------------------
endif
+66
View File
@@ -0,0 +1,66 @@
# Native Switch OTA launcher
In-console updates for Gen1Recomp on Nintendo Switch. This NRO is the **hbmenu
entry** (`gen1recomp.nro`). It checks GitHub Releases quietly (no console flash
when you are already up to date or offline). Only if a newer release exists
does it prompt to download the same `gen1recomp-*-switch.zip` used for install,
verify SHA-256 from `sha256sums.txt`, replace **both** `gen1recomp-game.nro`
and `gen1recomp.nro` (matching NACP version for hbmenu/Sphaira), then hand off
with `envSetNextLoad`.
The LÖVE self-updater (`src/update/Check.lua`) stays **disabled** on NX.
Protocol contract (also in Lua): `src/update/SwitchOta.lua`.
NACP icon: `assets/switch/icon.jpg`.
## Layout on microSD
```text
sdmc:/switch/gen1recomp/gen1recomp.nro ← this launcher
sdmc:/switch/gen1recomp/gen1recomp-game.nro ← fused LÖVE game
sdmc:/switch/gen1recomp/version.txt ← installed X.Y.Z
sdmc:/switch/gen1recomp/pokemon-love2d/ ← saves (never touched by OTA)
```
## Host tests (no DEVKITPRO)
```bash
cd native/switch-ota-launcher
make host-test
```
## Switch build (DEVKITPRO)
Needs `DEVKITPRO` with packages roughly:
```bash
(dkp-)pacman -S --needed switch-curl switch-mbedtls switch-zlib switch-zziplib
# or: bash scripts/switch/install_devkitpro_deps.sh
```
```bash
export DEVKITPRO=/opt/devkitpro # typical
cd native/switch-ota-launcher
make
# → gen1recomp.nro
```
Or from repo root:
```bash
scripts/switch/build_ota_launcher.sh
```
Docker fallback uses the same pin as fused builds (`scripts/switch/dkp-docker.image`).
## Packaging
`scripts/switch/pack_sd_zip.sh GAME_NRO VERSION OUT_ZIP LAUNCHER_NRO` writes both
NROs into the SD zip. That zip is also the OTA download asset (unified).
Manifest: `scripts/switch/ota_launcher.manifest`.
## Status / known gaps
- Zip extraction uses `switch-zziplib` (`ota_unzip.c`) on device.
- OTA replaces launcher + game from the unified zip (NACP versions stay aligned).
- Sphaira HOME forwarders cache metadata until reinstalled (see docs/switch-install.md).
- Install toolchain deps once: `bash scripts/switch/install_devkitpro_deps.sh`
@@ -0,0 +1,94 @@
/* Host-only unit tests for ota_protocol.c — no libnx. */
#include "ota_protocol.h"
#include <stdio.h>
#include <string.h>
static int g_fail = 0;
static void expect(int cond, const char *msg) {
if (!cond) {
fprintf(stderr, "FAIL: %s\n", msg);
g_fail++;
} else {
printf("PASS: %s\n", msg);
}
}
int main(void) {
expect(ota_compare_semver("1.2.0", "1.1.0") == 1, "semver newer");
expect(ota_compare_semver("1.1.0", "1.1.0") == 0, "semver equal");
expect(ota_compare_semver("1.0.0", "1.1.0") == -1, "semver older");
expect(ota_is_ota_asset_name("gen1recomp-1.5.0-switch.zip"), "ota asset name");
expect(!ota_is_ota_asset_name("gen1recomp-1.5.0-switch-ota.zip"), "reject legacy ota zip name");
expect(!ota_is_ota_asset_name("gen1recomp-1.5.0.love"), "reject love payload");
const char *json =
"{"
"\"tag_name\":\"v1.5.0\","
"\"assets\":["
"{\"name\":\"gen1recomp-1.5.0-switch.zip\","
"\"browser_download_url\":\"https://example/switch.zip\"},"
"{\"name\":\"sha256sums.txt\",\"browser_download_url\":\"https://example/sums\"}"
"]"
"}";
ota_release_t rel;
expect(ota_parse_release(json, &rel) == 1, "parse release");
expect(strcmp(rel.version, "1.5.0") == 0, "release version");
expect(strcmp(rel.asset_name, "gen1recomp-1.5.0-switch.zip") == 0, "release asset");
ota_decision_t d;
ota_decide_update("1.4.0", &rel, &d);
expect(strcmp(d.status, "available") == 0, "decide available");
ota_decide_update("1.5.0", &rel, &d);
expect(strcmp(d.status, "uptodate") == 0, "decide uptodate");
ota_release_t bad;
expect(ota_parse_release("{\"tag_name\":\"v1.5.0\",\"assets\":[]}", &bad) == 0,
"missing ota asset");
expect(strcmp(bad.reason, "missing_ota_asset") == 0, "missing_ota_asset reason");
const char *sums = "abc123 gen1recomp-1.5.0-switch.zip\n";
ota_verify_t v;
ota_verify_sha256("gen1recomp-1.5.0-switch.zip", "abc123", sums, &v);
expect(v.ok == 1, "sha ok");
ota_verify_sha256("gen1recomp-1.5.0-switch.zip", "deadbeef", sums, &v);
expect(v.ok == 0 && strcmp(v.reason, "hash_mismatch") == 0, "sha mismatch");
ota_verify_sha256("gen1recomp-1.5.0-switch.zip", "abc123", "", &v);
expect(v.ok == 0 && strcmp(v.reason, "sum_not_found") == 0, "sha missing sum rejected");
ota_apply_plan_t plan;
ota_plan_atomic_apply("switch/gen1recomp", "/tmp/x", &plan);
expect(strstr(plan.steps[0], "copy_to_part:") != NULL, "plan copy");
expect(strstr(plan.steps[1], "rename:") != NULL, "plan rename");
expect(strstr(plan.steps[2], "copy_to_part:launcher") != NULL, "plan launcher copy");
expect(strstr(plan.steps[3], "rename:") != NULL, "plan launcher rename");
expect(strstr(plan.steps[4], "env_set_next_load:") != NULL, "plan handoff");
expect(strstr(plan.preserve, "pokemon-love2d") != NULL, "preserve saves");
expect(strstr(plan.forbidden_delete, "delete:") != NULL, "forbid delete saves");
expect(strstr(plan.forbidden_direct, "write_direct:") != NULL, "forbid direct write");
ota_offline_t off;
ota_offline_events_t ev = {0};
ev.user_skip = 1;
ota_offline_policy(1.0, &ev, &off);
expect(strcmp(off.action, "play_installed") == 0, "skip plays installed");
ev.user_skip = 0;
ev.network_ok = 0;
ota_offline_policy(1.0, &ev, &off);
expect(strcmp(off.action, "play_installed") == 0, "offline plays installed");
ev.network_ok = 1;
ota_offline_policy(6.0, &ev, &off);
expect(strcmp(off.reason, "timeout") == 0, "timeout 6s");
ota_offline_policy(2.0, &ev, &off);
expect(strcmp(off.action, "keep_checking") == 0, "still checking");
expect(OTA_CHECK_TIMEOUT_SEC == 6, "timeout constant 6");
if (g_fail) {
fprintf(stderr, "%d failure(s)\n", g_fail);
return 1;
}
printf("all ota_protocol host tests passed\n");
return 0;
}
@@ -0,0 +1,32 @@
#ifndef GEN1_OTA_FS_H
#define GEN1_OTA_FS_H
#include "ota_protocol.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Read installed game version from sibling version.txt or NRO nacp-less stamp file.
* Expects switch/gen1recomp/version.txt containing X.Y.Z. Returns 0 on success. */
int ota_fs_read_installed_version(const char *install_dir, char *out, size_t out_len);
/* SHA-256 hex (lowercase) of file. Returns 0 on success. */
int ota_fs_sha256_file(const char *path, char *out_hex, size_t out_len);
/* Apply verified temp payload (extracted game NRO) using atomic plan. Returns 0 on success. */
int ota_fs_atomic_replace_game(const char *install_dir, const char *verified_game_nro,
char *err, size_t err_len);
/* Atomically replace any named NRO under install_dir (copy→.part→rename). */
int ota_fs_atomic_replace_nro(const char *install_dir, const char *nro_name,
const char *verified_nro, char *err, size_t err_len);
/* Hand off to game NRO via envSetNextLoad (Switch) or no-op stub (host). */
int ota_fs_handoff_to_game(const char *game_nro_path, char *err, size_t err_len);
#ifdef __cplusplus
}
#endif
#endif
@@ -0,0 +1,22 @@
#ifndef GEN1_OTA_NET_H
#define GEN1_OTA_NET_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Download URL into memory buffer (caller frees *out). Returns 0 on success. */
int ota_net_download_buffer(const char *url, long timeout_ms, char **out, size_t *out_len,
char *err, size_t err_len);
/* Download URL to a filesystem path. Returns 0 on success. */
int ota_net_download_file(const char *url, const char *path, long timeout_ms, char *err,
size_t err_len);
#ifdef __cplusplus
}
#endif
#endif
@@ -0,0 +1,92 @@
#ifndef GEN1_OTA_PROTOCOL_H
#define GEN1_OTA_PROTOCOL_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
#define OTA_CHECK_TIMEOUT_SEC 6
#define OTA_GAME_NRO_NAME "gen1recomp-game.nro"
#define OTA_LAUNCHER_NRO_NAME "gen1recomp.nro"
#define OTA_SAVE_DIR_NAME "pokemon-love2d"
#define OTA_INSTALL_DIR "switch/gen1recomp"
#define OTA_RELEASES_API \
"https://api.github.com/repos/bryanthaboi/gen1recomp/releases/latest"
/* Mirrors src/update/SwitchOta.lua — keep semantics in lockstep. */
int ota_compare_semver(const char *a, const char *b);
int ota_is_ota_asset_name(const char *name);
int ota_version_from_ota_asset(const char *name, char *out, size_t out_len);
typedef struct {
int ok; /* 1 on success */
char reason[64];
char tag[64];
char version[32];
char asset_name[128];
char download_url[512];
} ota_release_t;
int ota_parse_release(const char *json_text, ota_release_t *out);
typedef struct {
char status[32]; /* uptodate | available | error */
char reason[64];
char version[32];
char asset_name[128];
char download_url[512];
} ota_decision_t;
void ota_decide_update(const char *installed_version, const ota_release_t *release,
ota_decision_t *out);
/* sums: newline-separated sha256sums.txt body */
int ota_lookup_sum(const char *sums_text, const char *asset_name, char *out_hex,
size_t out_len);
typedef struct {
int ok;
char reason[64];
} ota_verify_t;
void ota_verify_sha256(const char *asset_name, const char *actual_hex,
const char *sums_text, ota_verify_t *out);
typedef struct {
char steps[5][520]; /* human-readable ops */
char preserve[256];
char forbidden_delete[256];
char forbidden_direct[256];
char part_path[256];
char game_nro[240];
char launcher_nro[240];
char launcher_part[256];
char next_load[240];
} ota_apply_plan_t;
void ota_plan_atomic_apply(const char *install_dir, const char *verified_temp,
ota_apply_plan_t *out);
typedef struct {
char action[32]; /* play_installed | keep_checking */
char reason[64];
char message[128];
} ota_offline_t;
typedef struct {
int user_skip;
int network_ok; /* 0 = offline/fail, 1 = ok, -1 = unknown */
int api_error;
} ota_offline_events_t;
void ota_offline_policy(double elapsed_sec, const ota_offline_events_t *events,
ota_offline_t *out);
#ifdef __cplusplus
}
#endif
#endif /* GEN1_OTA_PROTOCOL_H */
@@ -0,0 +1,20 @@
#ifndef GEN1_OTA_UNZIP_H
#define GEN1_OTA_UNZIP_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Extract member_name from zip_path into dest_path.
* Tries exact member_name, then common prefixes (switch/gen1recomp/).
* Returns 0 on success. */
int ota_unzip_extract_file(const char *zip_path, const char *member_name, const char *dest_path,
char *err, size_t err_len);
#ifdef __cplusplus
}
#endif
#endif
+330
View File
@@ -0,0 +1,330 @@
/*
* gen1recomp Switch OTA launcher
*
* Quiet by default: checks GitHub Releases with no console flash. Only shows
* a short prompt when an update is available. Downloads the same SD zip
* published for install (gen1recomp-*-switch.zip), verifies SHA-256, extracts
* switch/gen1recomp/gen1recomp-game.nro, then envSetNextLoad to the game.
* Never touches pokemon-love2d/. LÖVE self-updater stays off on NX.
*/
#include "ota_fs.h"
#include "ota_net.h"
#include "ota_protocol.h"
#include "ota_unzip.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if defined(__SWITCH__)
#include <sys/stat.h>
#include <switch.h>
#include <unistd.h>
#else
#include <sys/stat.h>
#include <unistd.h>
#endif
#define SD_INSTALL_DIR "sdmc:/switch/gen1recomp"
#define CHECK_TIMEOUT_MS (OTA_CHECK_TIMEOUT_SEC * 1000L)
#define GAME_MEMBER_IN_ZIP "switch/gen1recomp/" OTA_GAME_NRO_NAME
#define LAUNCHER_MEMBER_IN_ZIP "switch/gen1recomp/" OTA_LAUNCHER_NRO_NAME
#if defined(__SWITCH__)
static int g_ui = 0;
static void ui_begin(void) {
if (g_ui) return;
consoleInit(NULL);
consoleClear();
g_ui = 1;
}
static void ui_end(void) {
if (!g_ui) return;
consoleExit(NULL);
g_ui = 0;
}
static void ui_line(const char *msg) {
ui_begin();
printf("%s\n", msg);
consoleUpdate(NULL);
}
static void ui_blank(void) {
if (!g_ui) return;
consoleClear();
consoleUpdate(NULL);
}
#else
static void ui_begin(void) {}
static void ui_end(void) {}
static void ui_line(const char *msg) { fprintf(stderr, "%s\n", msg); }
static void ui_blank(void) {}
#endif
/* Returns 1 = update, 0 = skip. Only called when UI is already needed. */
static int wait_confirm_or_skip(void) {
#if defined(__SWITCH__)
PadState pad;
padInitializeDefault(&pad);
ui_line("");
ui_line(" A — Update");
ui_line(" B — Play without updating");
while (appletMainLoop()) {
padUpdate(&pad);
u64 k = padGetButtonsDown(&pad);
if (k & HidNpadButton_A) return 1;
if (k & HidNpadButton_B) return 0;
consoleUpdate(NULL);
}
return 0;
#else
return 0;
#endif
}
static int run_update_flow(const char *install_dir) {
char installed[64] = "0.0.0";
(void)ota_fs_read_installed_version(install_dir, installed, sizeof(installed));
char *json = NULL;
size_t json_len = 0;
char err[256];
err[0] = '\0';
if (ota_net_download_buffer(OTA_RELEASES_API, CHECK_TIMEOUT_MS, &json, &json_len, err,
sizeof(err)) != 0) {
free(json);
return 0; /* offline / timeout — play installed, no UI */
}
ota_release_t rel;
if (!ota_parse_release(json, &rel)) {
free(json);
return 0;
}
free(json);
ota_decision_t dec;
ota_decide_update(installed, &rel, &dec);
if (strcmp(dec.status, "uptodate") == 0 || strcmp(dec.status, "error") == 0) {
return 0;
}
/* Update available — only now show UI */
{
char msg[160];
ui_blank();
ui_line("");
ui_line(" gen1recomp");
ui_line("");
snprintf(msg, sizeof(msg), " Update available: %s → %s", installed, dec.version);
ui_line(msg);
}
if (!wait_confirm_or_skip()) {
ui_end();
return 0;
}
char updates_dir[192];
char zip_path[256];
char sums_path[256];
snprintf(updates_dir, sizeof(updates_dir), "%s/updates", install_dir);
snprintf(zip_path, sizeof(zip_path), "%s/updates/%s", install_dir, dec.asset_name);
snprintf(sums_path, sizeof(sums_path), "%s/updates/sha256sums.txt", install_dir);
#if defined(__SWITCH__)
mkdir(updates_dir, 0755);
#endif
ui_blank();
ui_line("");
ui_line(" Downloading…");
if (ota_net_download_file(dec.download_url, zip_path, 180000L, err, sizeof(err)) != 0) {
ui_line("");
ui_line(" Download failed.");
ui_line(" Playing the installed version.");
#if defined(__SWITCH__)
{
PadState pad;
padInitializeDefault(&pad);
ui_line("");
ui_line(" Press B to continue");
while (appletMainLoop()) {
padUpdate(&pad);
if (padGetButtonsDown(&pad) & HidNpadButton_B) break;
consoleUpdate(NULL);
}
}
#endif
ui_end();
return 0;
}
char sums_url[512];
snprintf(sums_url, sizeof(sums_url),
"https://github.com/bryanthaboi/gen1recomp/releases/download/%s/sha256sums.txt",
rel.tag);
if (ota_net_download_file(sums_url, sums_path, CHECK_TIMEOUT_MS, err, sizeof(err)) != 0) {
ui_line("");
ui_line(" Could not verify the update.");
ui_line(" Playing the installed version.");
remove(zip_path);
ui_end();
#if defined(__SWITCH__)
svcSleepThread(1200000000ULL);
#endif
return 0;
}
FILE *sf = fopen(sums_path, "rb");
if (!sf) {
remove(zip_path);
ui_end();
return 0;
}
fseek(sf, 0, SEEK_END);
long slen = ftell(sf);
fseek(sf, 0, SEEK_SET);
char *sums = (char *)malloc((size_t)slen + 1);
if (!sums) {
fclose(sf);
ui_end();
return 0;
}
fread(sums, 1, (size_t)slen, sf);
sums[slen] = '\0';
fclose(sf);
char hex[96];
if (ota_fs_sha256_file(zip_path, hex, sizeof(hex)) != 0) {
free(sums);
remove(zip_path);
ui_end();
return 0;
}
ota_verify_t ver;
ota_verify_sha256(dec.asset_name, hex, sums, &ver);
free(sums);
if (!ver.ok) {
ui_line("");
ui_line(" Update file failed checks.");
ui_line(" Playing the installed version.");
remove(zip_path);
ui_end();
#if defined(__SWITCH__)
svcSleepThread(1200000000ULL);
#endif
return 0;
}
char extracted[256];
char extracted_launcher[256];
snprintf(extracted, sizeof(extracted), "%s/updates/gen1recomp-game.nro.verified", install_dir);
snprintf(extracted_launcher, sizeof(extracted_launcher),
"%s/updates/gen1recomp.nro.verified", install_dir);
#if defined(__SWITCH__)
ui_line("");
ui_line(" Installing…");
/* Prefer path inside SD zip; fall back to bare name. */
if (ota_unzip_extract_file(zip_path, GAME_MEMBER_IN_ZIP, extracted, err, sizeof(err)) != 0) {
if (ota_unzip_extract_file(zip_path, OTA_GAME_NRO_NAME, extracted, err, sizeof(err)) != 0) {
ui_line("");
ui_line(" Install failed.");
ui_line(" Playing the installed version.");
remove(zip_path);
ui_end();
svcSleepThread(1200000000ULL);
return 0;
}
}
if (ota_fs_atomic_replace_game(install_dir, extracted, err, sizeof(err)) != 0) {
ui_line("");
ui_line(" Install failed.");
ui_line(" Playing the installed version.");
remove(extracted);
remove(zip_path);
ui_end();
svcSleepThread(1200000000ULL);
return 0;
}
remove(extracted);
/* Also replace launcher so hbmenu/Sphaira see the matching NACP version.
* Best-effort: game update already succeeded if this fails. */
if (ota_unzip_extract_file(zip_path, LAUNCHER_MEMBER_IN_ZIP, extracted_launcher, err,
sizeof(err)) == 0 ||
ota_unzip_extract_file(zip_path, OTA_LAUNCHER_NRO_NAME, extracted_launcher, err,
sizeof(err)) == 0) {
if (ota_fs_atomic_replace_nro(install_dir, OTA_LAUNCHER_NRO_NAME, extracted_launcher, err,
sizeof(err)) == 0) {
remove(extracted_launcher);
} else {
remove(extracted_launcher);
}
}
char vpath[192];
snprintf(vpath, sizeof(vpath), "%s/version.txt", install_dir);
FILE *vf = fopen(vpath, "wb");
if (vf) {
fprintf(vf, "%s\n", dec.version);
fclose(vf);
}
#else
(void)extracted;
(void)extracted_launcher;
#endif
remove(zip_path);
ui_end();
return 0;
}
int main(int argc, char **argv) {
(void)argc;
(void)argv;
#if defined(__SWITCH__)
socketInitializeDefault();
padConfigureInput(1, HidNpadStyleSet_NpadStandard);
#endif
const char *install = SD_INSTALL_DIR;
run_update_flow(install);
char game[192];
snprintf(game, sizeof(game), "%s/%s", install, OTA_GAME_NRO_NAME);
char err[128];
err[0] = '\0';
if (ota_fs_handoff_to_game(game, err, sizeof(err)) != 0) {
ui_line("");
ui_line(" gen1recomp");
ui_line("");
ui_line(" Game files are missing.");
ui_line(" Copy the Switch zip onto your microSD,");
ui_line(" then open gen1recomp again.");
#if defined(__SWITCH__)
ui_line("");
ui_line(" Press + to exit");
PadState pad;
padInitializeDefault(&pad);
while (appletMainLoop()) {
padUpdate(&pad);
if (padGetButtonsDown(&pad) & HidNpadButton_Plus) break;
consoleUpdate(NULL);
}
#endif
ui_end();
}
#if defined(__SWITCH__)
socketExit();
#endif
return 0;
}
+136
View File
@@ -0,0 +1,136 @@
#include "ota_fs.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if defined(__SWITCH__)
#include <switch.h>
#include <mbedtls/sha256.h>
#include <unistd.h>
#else
#include <unistd.h>
#endif
int ota_fs_read_installed_version(const char *install_dir, char *out, size_t out_len) {
if (!out || out_len == 0) return -1;
out[0] = '\0';
char path[256];
snprintf(path, sizeof(path), "%s/version.txt", install_dir ? install_dir : OTA_INSTALL_DIR);
FILE *fp = fopen(path, "rb");
if (!fp) return -1;
if (!fgets(out, (int)out_len, fp)) {
fclose(fp);
return -1;
}
fclose(fp);
/* trim */
size_t n = strlen(out);
while (n > 0 && (out[n - 1] == '\n' || out[n - 1] == '\r' || out[n - 1] == ' ')) {
out[--n] = '\0';
}
return n > 0 ? 0 : -1;
}
int ota_fs_sha256_file(const char *path, char *out_hex, size_t out_len) {
if (!path || !out_hex || out_len < 65) return -1;
out_hex[0] = '\0';
FILE *fp = fopen(path, "rb");
if (!fp) return -1;
#if defined(__SWITCH__)
mbedtls_sha256_context ctx;
mbedtls_sha256_init(&ctx);
mbedtls_sha256_starts(&ctx, 0);
unsigned char buf[8192];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) {
mbedtls_sha256_update(&ctx, buf, n);
}
fclose(fp);
unsigned char digest[32];
mbedtls_sha256_finish(&ctx, digest);
mbedtls_sha256_free(&ctx);
for (int i = 0; i < 32; i++) snprintf(out_hex + i * 2, out_len - (size_t)(i * 2), "%02x", digest[i]);
return 0;
#else
/* Host: shell out to shasum/sha256sum for the host test path if needed.
* Protocol host tests do not call this; return not-implemented. */
fclose(fp);
(void)out_len;
return -1;
#endif
}
static int copy_file(const char *from, const char *to) {
FILE *in = fopen(from, "rb");
if (!in) return -1;
FILE *out = fopen(to, "wb");
if (!out) {
fclose(in);
return -1;
}
char buf[8192];
size_t n;
while ((n = fread(buf, 1, sizeof(buf), in)) > 0) {
if (fwrite(buf, 1, n, out) != n) {
fclose(in);
fclose(out);
return -1;
}
}
fclose(in);
fclose(out);
return 0;
}
int ota_fs_atomic_replace_game(const char *install_dir, const char *verified_game_nro,
char *err, size_t err_len) {
return ota_fs_atomic_replace_nro(install_dir, OTA_GAME_NRO_NAME, verified_game_nro, err,
err_len);
}
int ota_fs_atomic_replace_nro(const char *install_dir, const char *nro_name,
const char *verified_nro, char *err, size_t err_len) {
if (!nro_name || !*nro_name || !verified_nro || !*verified_nro) {
if (err && err_len) snprintf(err, err_len, "missing nro paths");
return -1;
}
char dest[240];
char part[256];
snprintf(dest, sizeof(dest), "%s/%s", install_dir ? install_dir : OTA_INSTALL_DIR, nro_name);
snprintf(part, sizeof(part), "%s.part", dest);
if (copy_file(verified_nro, part) != 0) {
if (err && err_len) snprintf(err, err_len, "copy to .part failed (%s)", nro_name);
return -1;
}
#ifdef _WIN32
remove(dest);
#endif
if (rename(part, dest) != 0) {
if (err && err_len) snprintf(err, err_len, "rename .part -> %s failed", nro_name);
remove(part);
return -1;
}
return 0;
}
int ota_fs_handoff_to_game(const char *game_nro_path, char *err, size_t err_len) {
if (!game_nro_path || !*game_nro_path) {
if (err && err_len) snprintf(err, err_len, "missing game path");
return -1;
}
#if defined(__SWITCH__)
/* argv for next load: empty args string is fine */
Result rc = envSetNextLoad(game_nro_path, game_nro_path);
if (R_FAILED(rc)) {
if (err && err_len) snprintf(err, err_len, "envSetNextLoad failed: 0x%x", rc);
return -1;
}
return 0;
#else
if (err && err_len)
snprintf(err, err_len, "handoff stub (host): would envSetNextLoad %s", game_nro_path);
return 0; /* host stub succeeds for flow tests */
#endif
}
+119
View File
@@ -0,0 +1,119 @@
#include "ota_net.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if defined(__SWITCH__)
#include <curl/curl.h>
#include <switch.h>
#endif
#if defined(__SWITCH__)
struct mem_buf {
char *data;
size_t len;
};
static size_t write_mem(char *ptr, size_t size, size_t nmemb, void *userdata) {
struct mem_buf *m = (struct mem_buf *)userdata;
size_t n = size * nmemb;
char *p = (char *)realloc(m->data, m->len + n + 1);
if (!p) return 0;
m->data = p;
memcpy(m->data + m->len, ptr, n);
m->len += n;
m->data[m->len] = '\0';
return n;
}
static size_t write_file(char *ptr, size_t size, size_t nmemb, void *userdata) {
return fwrite(ptr, size, nmemb, (FILE *)userdata);
}
#endif
int ota_net_download_buffer(const char *url, long timeout_ms, char **out, size_t *out_len,
char *err, size_t err_len) {
if (out) *out = NULL;
if (out_len) *out_len = 0;
if (!url || !out) {
if (err && err_len) snprintf(err, err_len, "bad args");
return -1;
}
#if defined(__SWITCH__)
CURL *curl = curl_easy_init();
if (!curl) {
if (err && err_len) snprintf(err, err_len, "curl_easy_init failed");
return -1;
}
struct mem_buf mem = {0};
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "gen1recomp-switch-ota");
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout_ms);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, timeout_ms);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_mem);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &mem);
/* Atmosphere / homebrew CA store is limited; match common homebrew practice. */
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
CURLcode rc = curl_easy_perform(curl);
curl_easy_cleanup(curl);
if (rc != CURLE_OK) {
free(mem.data);
if (err && err_len) snprintf(err, err_len, "%s", curl_easy_strerror(rc));
return -1;
}
*out = mem.data;
if (out_len) *out_len = mem.len;
return 0;
#else
(void)timeout_ms;
if (err && err_len)
snprintf(err, err_len, "ota_net_download_buffer only available on __SWITCH__");
return -1;
#endif
}
int ota_net_download_file(const char *url, const char *path, long timeout_ms, char *err,
size_t err_len) {
if (!url || !path) {
if (err && err_len) snprintf(err, err_len, "bad args");
return -1;
}
#if defined(__SWITCH__)
FILE *fp = fopen(path, "wb");
if (!fp) {
if (err && err_len) snprintf(err, err_len, "fopen failed: %s", path);
return -1;
}
CURL *curl = curl_easy_init();
if (!curl) {
fclose(fp);
if (err && err_len) snprintf(err, err_len, "curl_easy_init failed");
return -1;
}
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_USERAGENT, "gen1recomp-switch-ota");
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout_ms);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, timeout_ms);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_file);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
CURLcode rc = curl_easy_perform(curl);
curl_easy_cleanup(curl);
fclose(fp);
if (rc != CURLE_OK) {
if (err && err_len) snprintf(err, err_len, "%s", curl_easy_strerror(rc));
return -1;
}
return 0;
#else
(void)timeout_ms;
if (err && err_len)
snprintf(err, err_len, "ota_net_download_file only available on __SWITCH__");
return -1;
#endif
}
@@ -0,0 +1,292 @@
#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");
}
@@ -0,0 +1,89 @@
#include "ota_unzip.h"
#include <stdio.h>
#include <string.h>
#if defined(__SWITCH__)
#include <fcntl.h>
#include <zzip/zzip.h>
#endif
#if defined(__SWITCH__)
static int extract_member(ZZIP_DIR *dir, const char *member, const char *dest_path, char *err,
size_t err_len) {
ZZIP_FILE *zf = zzip_file_open(dir, member, O_RDONLY);
if (!zf) return 1; /* not found / cannot open */
FILE *out = fopen(dest_path, "wb");
if (!out) {
zzip_file_close(zf);
if (err && err_len) snprintf(err, err_len, "fopen dest failed: %s", dest_path);
return -1;
}
char buf[8192];
zzip_ssize_t n;
while ((n = zzip_file_read(zf, buf, sizeof(buf))) > 0) {
if ((zzip_ssize_t)fwrite(buf, 1, (size_t)n, out) != n) {
fclose(out);
zzip_file_close(zf);
if (err && err_len) snprintf(err, err_len, "fwrite failed");
return -1;
}
}
fclose(out);
zzip_file_close(zf);
if (n < 0) {
if (err && err_len) snprintf(err, err_len, "zzip_file_read failed");
return -1;
}
return 0;
}
#endif
int ota_unzip_extract_file(const char *zip_path, const char *member_name, const char *dest_path,
char *err, size_t err_len) {
if (!zip_path || !member_name || !dest_path) {
if (err && err_len) snprintf(err, err_len, "bad args");
return -1;
}
#if defined(__SWITCH__)
zzip_error_t zerr = ZZIP_NO_ERROR;
ZZIP_DIR *dir = zzip_dir_open(zip_path, &zerr);
if (!dir) {
if (err && err_len) snprintf(err, err_len, "zzip_dir_open failed (%d): %s", (int)zerr, zip_path);
return -1;
}
char alt1[192];
char alt2[192];
snprintf(alt1, sizeof(alt1), "switch/gen1recomp/%s", member_name);
snprintf(alt2, sizeof(alt2), "./%s", member_name);
const char *candidates[] = {member_name, alt1, alt2, NULL};
int rc = -1;
if (err && err_len) err[0] = '\0';
for (int i = 0; candidates[i]; i++) {
int t = extract_member(dir, candidates[i], dest_path, err, err_len);
if (t == 0) {
rc = 0;
break;
}
if (t < 0) {
rc = -1;
break;
}
}
if (rc != 0 && err && err_len && !err[0]) {
snprintf(err, err_len, "member not found in zip: %s", member_name);
}
zzip_dir_close(dir);
return rc;
#else
(void)zip_path;
(void)member_name;
(void)dest_path;
if (err && err_len) snprintf(err, err_len, "ota_unzip only available on __SWITCH__");
return -1;
#endif
}