diff --git a/INSTALLATION_PROCESS.md b/INSTALLATION_PROCESS.md index b41868a..3c9b5cb 100644 --- a/INSTALLATION_PROCESS.md +++ b/INSTALLATION_PROCESS.md @@ -90,7 +90,9 @@ The OmniNX Installer Payload operates in two modes: **Trigger**: `INSTALL_MODE_UPDATE` (OmniNX already installed) ### Step 1: Cleanup (Selective Deletion) -**Location**: `install.c:321-380` +**Location**: `install_update.c` (`update_mode_cleanup`), `install.c` (`delete_path_lists_grouped`) + +Before deletion, the payload reads optional **`sd:/config/omninx/preserve.ini`** (section `[Preserve]`). Paths enabled there are skipped, as are delete targets that would remove them (parent/child overlap). See **[PRESERVE_INI.md](PRESERVE_INI.md)**. Clean install does not use this file. #### 10. Clean Atmosphere Subdirectories **Location**: `install.c:325-327`, `deletion_lists.h:9-34` diff --git a/PRESERVE_INI.md b/PRESERVE_INI.md new file mode 100644 index 0000000..2ce55fc --- /dev/null +++ b/PRESERVE_INI.md @@ -0,0 +1,59 @@ +# User preserve list (`preserve.ini`) + +**Update mode only.** During OmniNX update cleanup (selective deletion), any path listed here with a truthy value is **not** deleted. Paths that would remove a preserved path (parent directory) are skipped as well. + +- **File:** `sd:/config/omninx/preserve.ini` +- If missing or invalid, behavior is unchanged (full update deletion list applies). + +## Format + +Section **`[Preserve]`** (case-sensitive). Each line is **path as key**, **enable flag as value**: + +```ini +[Preserve] +'sd:/ROMs'=1 +sd:/switch/MyHomebrewFolder=0 +sd:/backup/tools=1 +``` + +### Path (key) + +- Must start with **`sd:/`** (after optional quoting). +- **Optional** matching **single** or **double** quotes around the key are stripped (helps Ultrahand `set-ini-val` and readability). +- Trailing slashes are normalized away (except the volume root). +- **`sd:/` alone** is rejected (too broad). +- Minimum useful length after `sd:/` is enforced (at least one more character). + +### Value (enable) + +**On** if the value starts with `1`, `t`, `T`, `y`, or `Y` (e.g. `1`, `true`, `yes`). Anything else is **off** (path ignored). + +### Limits + +- Up to **96** preserved paths; longer keys truncated internally (~224 bytes per path). +- Duplicate paths after normalization are skipped. +- The list is allocated on the **heap** only while update cleanup runs (not a large static buffer in IPL IRAM), to avoid crowding hardware-adjacent memory used by Joy-Con UART and similar peripherals. + +## Overlap rules + +For each delete candidate from `deletion_lists_update.h`, deletion is skipped if it **overlaps** any enabled preserve path: + +- Same path, or +- Delete target is **inside** a preserved tree (preserved path is a prefix), or +- Delete target is an **ancestor** of a preserved path (deleting it would remove the preserved data). + +So preserving `sd:/ROMs` also blocks deleting `sd:/ROMs/title` if that were on the delete list, and blocks deleting `sd:/` if `sd:/ROMs` is preserved. + +## Ultrahand + +You can toggle entries with: + +```text +set-ini-val sd:/config/omninx/preserve.ini Preserve 'sd:/ROMs' 1 +``` + +and `0` to disable. Use the same quoted path in the key argument as in your INI. + +## Clean install + +**Not used** in clean install mode. `preserve.ini` is only read for **update** cleanup (`update_mode_cleanup`). diff --git a/README.md b/README.md index 453948a..cb138e0 100644 --- a/README.md +++ b/README.md @@ -23,12 +23,14 @@ Based on [HATS-Installer-Payload](https://github.com/sthetix/HATS-Installer-Payl For detailed information about the installation process, see: - **[INSTALLATION_PROCESS.md](INSTALLATION_PROCESS.md)** - Complete step-by-step breakdown of everything checked and done during installation/update - **[DEBUG_INI.md](DEBUG_INI.md)** - Optional `sd:/config/omninx/debug.ini` parameters to skip install steps (for testing) +- **[PRESERVE_INI.md](PRESERVE_INI.md)** - Optional `sd:/config/omninx/preserve.ini` to skip deleting chosen paths in **update** mode ## Installation Modes ### Update Mode (OmniNX Detected) - Detected when version marker files (`1.0.0s`, `1.0.0l`, or `1.0.0oc`) are found - Performs selective deletion of specific directories/files +- Optional **`sd:/config/omninx/preserve.ini`**: user-listed `sd:/…` paths (with `=1`) are excluded from that cleanup (see [PRESERVE_INI.md](PRESERVE_INI.md)) - Preserves user data, savegames, and installed games - Updates only necessary CFW components diff --git a/source/deletion_lists_update.h b/source/deletion_lists_update.h index 5a188a0..2cddd44 100644 --- a/source/deletion_lists_update.h +++ b/source/deletion_lists_update.h @@ -10,14 +10,16 @@ static const char* atmosphere_dirs_to_delete[] = { "sd:/atmosphere/config", "sd:/atmosphere/crash_reports", "sd:/atmosphere/erpt_reports", + "sd:/atmosphere/exefs_patches/audio_mastervolume", "sd:/atmosphere/exefs_patches/CrunchPatch", + "sd:/atmosphere/exefs_patches/SaltyNX_Fixes", "sd:/atmosphere/exefs_patches/Crunchyroll Patch 1.10.0", "sd:/atmosphere/exefs_patches/bluetooth_patches", "sd:/atmosphere/exefs_patches/bootlogo", "sd:/atmosphere/exefs_patches/btm_patches", "sd:/atmosphere/exefs_patches/es_patches", "sd:/atmosphere/exefs_patches/hid_patches", - "sd:/atmosphere/exefs_patches/logo1", + "sd:/atmosphere/exefs_patches/logo", "sd:/atmosphere/exefs_patches/nfim_ctest", "sd:/atmosphere/exefs_patches/nim_ctest", "sd:/atmosphere/exefs_patches/nvnflinger_cmu", diff --git a/source/install.c b/source/install.c index 5afde0d..0d32424 100644 --- a/source/install.c +++ b/source/install.c @@ -21,6 +21,8 @@ #define GROUPED_DELETE_MAX_ENTRIES 512 +static bool install_path_blocked_by_user_preserve(const char *path); + #ifndef VERSION #define VERSION "1.0.0" #endif @@ -463,7 +465,7 @@ int delete_path_lists_grouped(const char *folder_display_name, ...) { const char **list; while (n_entries < GROUPED_DELETE_MAX_ENTRIES && (list = va_arg(ap, const char **)) != NULL) { for (int i = 0; list[i] != NULL && n_entries < GROUPED_DELETE_MAX_ENTRIES; i++) { - if (install_path_exists(list[i])) + if (install_path_exists(list[i]) && !install_path_blocked_by_user_preserve(list[i])) entries[n_entries++] = list[i]; } } @@ -538,9 +540,9 @@ int delete_path_list(const char* paths[], const char* description) { u32 start_x, start_y; int last_percent = -1; - // Count total paths first + // Count total paths first (only those we will delete) for (int i = 0; paths[i] != NULL; i++) { - if (install_path_exists(paths[i])) { + if (install_path_exists(paths[i]) && !install_path_blocked_by_user_preserve(paths[i])) { total_paths++; } } @@ -558,7 +560,7 @@ int delete_path_list(const char* paths[], const char* description) { install_set_color(COLOR_WHITE); for (int i = 0; paths[i] != NULL; i++) { - if (install_path_exists(paths[i])) { + if (install_path_exists(paths[i]) && !install_path_blocked_by_user_preserve(paths[i])) { FILINFO fno; f_stat(paths[i], &fno); @@ -686,6 +688,159 @@ static void ram_config_free_sections(link_t *sections) } } +#define INSTALL_PRESERVE_INI "sd:/config/omninx/preserve.ini" +#define INSTALL_PRESERVE_MAX 96 +#define INSTALL_PRESERVE_PATH_BYTES 224 + +static bool install_ini_kv_truth(const char *val) +{ + if (!val || !val[0]) + return false; + char c = val[0]; + return c == '1' || c == 't' || c == 'T' || c == 'y' || c == 'Y'; +} + +static void preserve_strip_outer_quotes(char *s) +{ + size_t n = strlen(s); + if (n >= 2 && + ((s[0] == '\'' && s[n - 1] == '\'') || (s[0] == '"' && s[n - 1] == '"'))) { + memmove(s, s + 1, n - 2); + s[n - 2] = '\0'; + } +} + +static void preserve_normalize_trailing_slashes(char *s) +{ + size_t n = strlen(s); + while (n > 1 && s[n - 1] == '/') + s[--n] = '\0'; +} + +static bool path_is_same_or_descendant(const char *base, const char *path) +{ + size_t lb = strlen(base); + if (strncmp(path, base, lb) != 0) + return false; + if (path[lb] == '\0') + return true; + if (path[lb] == '/') + return true; + if (lb == 4 && base[0] == 's' && base[1] == 'd' && base[2] == ':' && base[3] == '/') + return path[lb] != '\0'; + return false; +} + +static bool path_delete_overlaps_preserve(const char *del, const char *pres) +{ + return path_is_same_or_descendant(pres, del) || path_is_same_or_descendant(del, pres); +} + +static bool g_preserve_update_active; +/** Rows stored in DRAM (heap); avoids ~21KB BSS in tight IPL IRAM around 0x40008000. */ +static char *g_preserve_block; +static int g_preserve_count; + +static const char *preserve_get_row(int i) +{ + return g_preserve_block + (size_t)i * INSTALL_PRESERVE_PATH_BYTES; +} + +static bool install_path_blocked_by_user_preserve(const char *delete_path) +{ + if (!g_preserve_update_active || !g_preserve_block || !delete_path) + return false; + for (int i = 0; i < g_preserve_count; i++) { + if (path_delete_overlaps_preserve(delete_path, preserve_get_row(i))) + return true; + } + return false; +} + +void install_preserve_update_cleanup_begin(void) +{ + if (g_preserve_block) { + free(g_preserve_block); + g_preserve_block = NULL; + } + g_preserve_count = 0; + g_preserve_update_active = false; + + if (!install_path_exists(INSTALL_PRESERVE_INI)) + return; + + g_preserve_block = calloc(INSTALL_PRESERVE_MAX, INSTALL_PRESERVE_PATH_BYTES); + if (!g_preserve_block) + return; + + link_t sections; + list_init(§ions); + if (ini_parse(§ions, (char *)INSTALL_PRESERVE_INI, false) != 1) { + ram_config_free_sections(§ions); + free(g_preserve_block); + g_preserve_block = NULL; + return; + } + + LIST_FOREACH_ENTRY(ini_sec_t, sec, §ions, link) { + if (!sec->name || strcmp(sec->name, "Preserve") != 0) + continue; + LIST_FOREACH_ENTRY(ini_kv_t, kv, &sec->kvs, link) { + if (!kv->key || !kv->val || g_preserve_count >= INSTALL_PRESERVE_MAX) + continue; + if (!install_ini_kv_truth(kv->val)) + continue; + char buf[INSTALL_PRESERVE_PATH_BYTES]; + strncpy(buf, kv->key, sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + preserve_strip_outer_quotes(buf); + preserve_normalize_trailing_slashes(buf); + if (strlen(buf) < 5 || strncmp(buf, "sd:/", 4) != 0) + continue; + if (strcmp(buf, "sd:/") == 0) + continue; + + bool dup = false; + for (int j = 0; j < g_preserve_count; j++) { + if (strcmp(preserve_get_row(j), buf) == 0) { + dup = true; + break; + } + } + if (dup) + continue; + + char *row = (char *)preserve_get_row(g_preserve_count); + strncpy(row, buf, INSTALL_PRESERVE_PATH_BYTES - 1); + row[INSTALL_PRESERVE_PATH_BYTES - 1] = '\0'; + g_preserve_count++; + } + break; + } + ram_config_free_sections(§ions); + + if (g_preserve_count == 0) { + free(g_preserve_block); + g_preserve_block = NULL; + return; + } + + g_preserve_update_active = true; + install_set_color(COLOR_CYAN); + gfx_printf(" preserve.ini: %d Pfad(e) von Update-Bereinigung ausgenommen\n", g_preserve_count); + install_set_color(COLOR_WHITE); +} + +void install_preserve_update_cleanup_end(void) +{ + if (g_preserve_block) { + free(g_preserve_block); + g_preserve_block = NULL; + } + g_preserve_count = 0; + g_preserve_update_active = false; +} + /* Valid [Ram] 8gb=0|1 → silent; missing file or invalid → menu. */ static int read_ram_config_silent(const char *path, bool *use_8gb) { @@ -884,14 +1039,6 @@ typedef struct { bool skip_hekate_8gb_post_copy; } install_debug_opts_t; -static bool install_ini_truth(const char *val) -{ - if (!val || !val[0]) - return false; - char c = val[0]; - return c == '1' || c == 't' || c == 'T' || c == 'y' || c == 'Y'; -} - static void install_debug_load(install_debug_opts_t *d) { memset(d, 0, sizeof(*d)); @@ -912,25 +1059,25 @@ static void install_debug_load(install_debug_opts_t *d) if (!kv->key || !kv->val) continue; if (strcmp(kv->key, "skip_clean_backup") == 0) - d->skip_clean_backup = install_ini_truth(kv->val); + d->skip_clean_backup = install_ini_kv_truth(kv->val); else if (strcmp(kv->key, "skip_clean_wipe") == 0) - d->skip_clean_wipe = install_ini_truth(kv->val); + d->skip_clean_wipe = install_ini_kv_truth(kv->val); else if (strcmp(kv->key, "skip_clean_restore") == 0) - d->skip_clean_restore = install_ini_truth(kv->val); + d->skip_clean_restore = install_ini_kv_truth(kv->val); else if (strcmp(kv->key, "skip_clean_install") == 0) - d->skip_clean_install = install_ini_truth(kv->val); + d->skip_clean_install = install_ini_kv_truth(kv->val); else if (strcmp(kv->key, "skip_clean_staging_cleanup") == 0) - d->skip_clean_staging_cleanup = install_ini_truth(kv->val); + d->skip_clean_staging_cleanup = install_ini_kv_truth(kv->val); else if (strcmp(kv->key, "skip_update_cleanup") == 0) - d->skip_update_cleanup = install_ini_truth(kv->val); + d->skip_update_cleanup = install_ini_kv_truth(kv->val); else if (strcmp(kv->key, "skip_update_install") == 0) - d->skip_update_install = install_ini_truth(kv->val); + d->skip_update_install = install_ini_kv_truth(kv->val); else if (strcmp(kv->key, "skip_update_staging_cleanup") == 0) - d->skip_update_staging_cleanup = install_ini_truth(kv->val); + d->skip_update_staging_cleanup = install_ini_kv_truth(kv->val); else if (strcmp(kv->key, "skip_update_horizon_oc") == 0) - d->skip_update_horizon_oc = install_ini_truth(kv->val); + d->skip_update_horizon_oc = install_ini_kv_truth(kv->val); else if (strcmp(kv->key, "skip_hekate_8gb_post_copy") == 0) - d->skip_hekate_8gb_post_copy = install_ini_truth(kv->val); + d->skip_hekate_8gb_post_copy = install_ini_kv_truth(kv->val); } break; } diff --git a/source/install.h b/source/install.h index 3e0a2d8..b63363b 100644 --- a/source/install.h +++ b/source/install.h @@ -32,6 +32,9 @@ int clean_mode_wipe(void); int clean_mode_restore(void); int clean_mode_install(omninx_variant_t variant); +void install_preserve_update_cleanup_begin(void); +void install_preserve_update_cleanup_end(void); + // Shared helpers (install.c) - used by install_update.c and install_clean.c void install_set_color(u32 color); void install_check_and_clear_screen_if_needed(void); diff --git a/source/install_update.c b/source/install_update.c index 94d40a3..247d865 100644 --- a/source/install_update.c +++ b/source/install_update.c @@ -37,6 +37,8 @@ // Update mode: Cleanup specific directories/files int update_mode_cleanup(omninx_variant_t variant) { (void)variant; + install_preserve_update_cleanup_begin(); + check_and_clear_screen_if_needed(); set_color(COLOR_WHITE); @@ -66,6 +68,8 @@ int update_mode_cleanup(omninx_variant_t variant) { misc_files_to_delete, NULL); + install_preserve_update_cleanup_end(); + set_color(COLOR_GREEN); gfx_printf(" Bereinigung abgeschlossen!\n"); set_color(COLOR_WHITE); diff --git a/source/main.c b/source/main.c index 56f3f88..dcfbaec 100644 --- a/source/main.c +++ b/source/main.c @@ -145,7 +145,7 @@ static int launch_payload(const char *path) { if (sd_mount()) { FIL fp; if (f_open(&fp, path, FA_READ)) { - gfx_printf("Payload nicht gefunden: %s\n", path); + gfx_printf("Hekate nicht gefunden: %s\n", path); return 1; } @@ -301,7 +301,7 @@ void ipl_main(void) { if (file_exists(PAYLOAD_PATH)) { gfx_printf("\n"); set_color(COLOR_CYAN); - gfx_printf("Payload wird gestartet...\n"); + gfx_printf("Hekate wird gestartet...\n"); set_color(COLOR_WHITE); msleep(500); launch_payload(PAYLOAD_PATH); @@ -721,16 +721,16 @@ void ipl_main(void) { gfx_printf("\n"); set_color(COLOR_CYAN); - gfx_printf("Payload wird gestartet...\n"); + gfx_printf("Hekate wird gestartet...\n"); set_color(COLOR_WHITE); msleep(500); // Brief delay before launch if (file_exists(PAYLOAD_PATH)) { launch_payload(PAYLOAD_PATH); } else { - // Payload not found, show error + // Hekate (update.bin) not found, show error set_color(COLOR_RED); - gfx_printf("\nFEHLER: Payload nicht gefunden!\n"); + gfx_printf("\nFEHLER: Hekate nicht gefunden!\n"); gfx_printf("Pfad: %s\n", PAYLOAD_PATH); set_color(COLOR_WHITE); gfx_printf("\nDruecke A oder Power zum Neustart...\n");