diff --git a/.github/workflows/build-jobs.yaml b/.github/workflows/build-jobs.yaml index dfd713e..fceda0a 100644 --- a/.github/workflows/build-jobs.yaml +++ b/.github/workflows/build-jobs.yaml @@ -20,24 +20,40 @@ jobs: - name: Build sys-patch run: | - make -C sys-patch -j$(nproc) dist && \ + + LIBNX_VER=$(dkp-pacman -Q libnx | cut -d' ' -f2 | cut -d- -f1) + echo "Detected libnx version: $LIBNX_VER" + + if [ "$LIBNX_VER" = "4.11.0" ]; then + EXTRA_FLAGS="-DUSE_DEBUG_EVENT=1" + else + EXTRA_FLAGS="" + fi + + make -C sys-patch -j$(nproc) dist EXTRA_FLAGS="$EXTRA_FLAGS" && \ + VERSION=$(grep 'export VERSION := ' sys-patch/Makefile | cut -c 19-) TAGVERSION=$(curl -s https://api.github.com/repos/$GITHUB_REPOSITORY/releases/latest | grep "tag_name" | head -1 | cut -d '"' -f 4) echo "VERSION=${VERSION}" >> $GITHUB_ENV echo "TAGVERSION=${TAGVERSION}" >> $GITHUB_ENV + mv sys-patch/sys-patch.zip sys-patch/sys-patch-v$VERSION.zip + SHA256=$(sha256sum sys-patch/sys-patch-v$VERSION.zip) + echo "SHA256=${SHA256}" >> $GITHUB_ENV - name: Upload artifact uses: actions/upload-artifact@v4 with: include-hidden-files: true overwrite: true - name: sys-patch-${{ env.VERSION }} + name: sys-patch-v${{ env.VERSION }} path: sys-patch/out/ - name: Fetch git cli and upload release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | + echo "sys-patch/sys-patch-v${{ env.VERSION }}.zip" + echo "${{ env.SHA256 }}" if [ ${{ env.TAGVERSION }} = v${{ env.VERSION }} ]; then echo "Tag version and makefile version are same, don't publish release, only artifact uploaded." else @@ -47,6 +63,6 @@ jobs: chmod +x gh*/bin/gh && \ cp gh*/bin/gh /bin/gh && \ rm gh*.tar.gz && \ - rm -rf gh* - gh release create v${{ env.VERSION }} sys-patch/sys-patch.zip --title "Sys-patch version ${{ env.VERSION }}" --repo github.com/$GITHUB_REPOSITORY + rm -rf gh* && \ + gh release create v${{ env.VERSION }} sys-patch/sys-patch-v${{ env.VERSION }}.zip --title "Sys-patch version v${{ env.VERSION }}" --repo github.com/$GITHUB_REPOSITORY fi diff --git a/.gitmodules b/.gitmodules index 2088864..050d296 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,7 @@ path = overlay/libtesla url = https://github.com/WerWolv/libtesla branch = master +[submodule "overlay/libultrahand"] + path = overlay/libultrahand + url = https://github.com/ppkantorski/libultrahand + branch = main diff --git a/Makefile b/Makefile index f760ec7..567d756 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ MAKEFILES := sysmod overlay TARGETS := $(foreach dir,$(MAKEFILES),$(CURDIR)/$(dir)) # the below was taken from atmosphere + switch-examples makefile -export VERSION := 1.5.9 +export VERSION := 1.6.0 ifneq ($(strip $(shell git symbolic-ref --short HEAD 2>/dev/null)),) export GIT_BRANCH := $(shell git symbolic-ref --short HEAD) diff --git a/overlay/Makefile b/overlay/Makefile index 382a67d..7224324 100644 --- a/overlay/Makefile +++ b/overlay/Makefile @@ -37,17 +37,20 @@ include $(DEVKITPRO)/libnx/switch_rules # of a homebrew executable (.nro). This is intended to be used for sysmodules. # NACP building is skipped as well. #--------------------------------------------------------------------------------- -APP_TITLE := sys-patch -APP_AUTHOR := TotalJustice -APP_VERSION := $(VERSION_DIRTY) +APP_TITLE := sys-patch +APP_AUTHOR := TotalJustice +APP_VERSION := $(VERSION_DIRTY) -TARGET := sys-patch-overlay -BUILD := build -SOURCES := src ../common/minIni -DATA := data -INCLUDES := include ../common libtesla/include +TARGET := sys-patch-overlay +BUILD := build -NO_ICON := 1 +SOURCES := src common +DATA := data +INCLUDES := src common include lib/libultrahand/include + +NO_ICON := 1 + +include $(TOPDIR)/libultrahand/ultrahand.mk #--------------------------------------------------------------------------------- # options for code generation diff --git a/overlay/libultrahand b/overlay/libultrahand new file mode 160000 index 0000000..70aadae --- /dev/null +++ b/overlay/libultrahand @@ -0,0 +1 @@ +Subproject commit 70aadae3b1e452bd9781e0a12e619505bd6ac3d7 diff --git a/overlay/src/main.cpp b/overlay/src/main.cpp index eb44735..40476a7 100644 --- a/overlay/src/main.cpp +++ b/overlay/src/main.cpp @@ -1,218 +1,234 @@ -#define TESLA_INIT_IMPL // If you have more than one file using the tesla header, only define this in the main one +/** + * sys-patch overlay - ported to libultrahand + * + * Original: https://github.com/ITotalJustice/sys-patch (libtesla) + * Ported to use: https://github.com/ppkantorski/libultrahand + * + * Key changes from libtesla → libultrahand: + * - Replace #include with #include + #include + * - Replace minIni (ini_getbool / ini_putl / ini_browse) with libultrahand's + * getParsedDataFromIniFile() / setIniValue() / saveIniFileData() + * - Replace raw FsFileSystem/FsFile SD-card helpers with ult::isFile() and + * ult::createDirectory() + * - Add `using namespace ult;` so all ultrahand helpers are in scope + */ + +#define TESLA_INIT_IMPL #define STBTT_STATIC -#include // The Tesla Header +#define NDEBUG + +#include // libultrahand core (provides ult:: namespace) +#include // libtesla UI layer (still needed for tsl::elm::*) #include -#include "minIni/minIni.h" -namespace { +using namespace ult; // bring getParsedDataFromIniFile, setIniValue, isFile, … into scope -constexpr auto CONFIG_PATH = "/config/sys-patch/config.ini"; -constexpr auto LOG_PATH = "/config/sys-patch/log.ini"; +namespace { -auto does_file_exist(const char* path) -> bool { - Result rc{}; - FsFileSystem fs{}; - FsFile file{}; - char path_buf[FS_MAX_PATH]{}; +// ── paths ──────────────────────────────────────────────────────────────────── +constexpr const char* CONFIG_PATH = "sdmc:/config/sys-patch/config.ini"; +constexpr const char* LOG_PATH = "sdmc:/config/sys-patch/log.ini"; - if (R_FAILED(fsOpenSdCardFileSystem(&fs))) { - return false; - } +// ── helpers ────────────────────────────────────────────────────────────────── - strcpy(path_buf, path); - rc = fsFsOpenFile(&fs, path_buf, FsOpenMode_Read, &file); - fsFileClose(&file); - fsFsClose(&fs); - return R_SUCCEEDED(rc); +/** + * Ensure a directory exists on the SD card. + * Uses libultrahand's createDirectory() (ult::) instead of raw FsFileSystem. + */ +inline void ensureDir(const char* path) { + createDirectory(path); // ult:: – no-op if it already exists } -// creates a directory, non-recursive! -auto create_dir(const char* path) -> bool { - Result rc{}; - FsFileSystem fs{}; - char path_buf[FS_MAX_PATH]{}; - - if (R_FAILED(fsOpenSdCardFileSystem(&fs))) { - return false; - } - - strcpy(path_buf, path); - rc = fsFsCreateDirectory(&fs, path_buf); - fsFsClose(&fs); - return R_SUCCEEDED(rc); -} +// ── ConfigEntry ────────────────────────────────────────────────────────────── +/** + * Represents a single boolean toggle stored in CONFIG_PATH. + * + * libtesla version used minIni (ini_getbool / ini_putl). + * libultrahand version uses: + * - getParsedDataFromIniFile() to read + * - setIniValue() to write a single key + */ struct ConfigEntry { - ConfigEntry(const char* _section, const char* _key, bool default_value) : - section{_section}, key{_key}, value{default_value} { - this->load_value_from_ini(); - } + const char* const section; + const char* const key; + bool value; + + ConfigEntry(const char* section_, const char* key_, bool default_value) + : section{section_}, key{key_}, value{default_value} + { + load_value_from_ini(); + } void load_value_from_ini() { - this->value = ini_getbool(this->section, this->key, this->value, CONFIG_PATH); + // getParsedDataFromIniFile returns map> + const auto iniData = getParsedDataFromIniFile(CONFIG_PATH); + auto secIt = iniData.find(section); + if (secIt != iniData.end()) { + auto keyIt = secIt->second.find(key); + if (keyIt != secIt->second.end()) { + const std::string& v = keyIt->second; + value = (v == "1" || v == "true" || v == "yes"); + return; + } + } + // key absent – keep default; write it so it exists next time + setIniFileValue(CONFIG_PATH, section, key, value ? "1" : "0"); } auto create_list_item(const char* text) { auto item = new tsl::elm::ToggleListItem(text, value); - item->setStateChangedListener([this](bool new_value){ + item->setStateChangedListener([this](bool new_value) { this->value = new_value; - ini_putl(this->section, this->key, this->value, CONFIG_PATH); + // setIniValue writes a single key in-place (creates file if needed) + setIniFileValue(CONFIG_PATH, this->section, this->key, new_value ? "1" : "0"); }); return item; } - - const char* const section; - const char* const key; - bool value; }; +// ── GuiOptions ─────────────────────────────────────────────────────────────── class GuiOptions final : public tsl::Gui { public: - GuiOptions() { } - tsl::elm::Element* createUI() override { auto frame = new tsl::elm::OverlayFrame("sys-patch", VERSION_WITH_HASH); - auto list = new tsl::elm::List(); + auto list = new tsl::elm::List(); list->addItem(new tsl::elm::CategoryHeader("Options")); - list->addItem(config_patch_sysmmc.create_list_item("Patch sysMMC")); - list->addItem(config_patch_emummc.create_list_item("Patch emuMMC")); - list->addItem(config_logging.create_list_item("Logging")); - list->addItem(config_version_skip.create_list_item("Version skip")); + list->addItem(config_patch_sysmmc .create_list_item("Patch sysMMC")); + list->addItem(config_patch_emummc .create_list_item("Patch emuMMC")); + list->addItem(config_logging .create_list_item("Logging")); + list->addItem(config_version_skip .create_list_item("Version skip")); frame->setContent(list); return frame; } - ConfigEntry config_patch_sysmmc{"options", "patch_sysmmc", true}; - ConfigEntry config_patch_emummc{"options", "patch_emummc", true}; - ConfigEntry config_logging{"options", "enable_logging", true}; - ConfigEntry config_version_skip{"options", "version_skip", true}; +private: + ConfigEntry config_patch_sysmmc {"options", "patch_sysmmc", true}; + ConfigEntry config_patch_emummc {"options", "patch_emummc", true}; + ConfigEntry config_logging {"options", "enable_logging", true}; + ConfigEntry config_version_skip {"options", "version_skip", true}; }; +// ── GuiToggle ──────────────────────────────────────────────────────────────── class GuiToggle final : public tsl::Gui { public: - GuiToggle() { } - tsl::elm::Element* createUI() override { auto frame = new tsl::elm::OverlayFrame("sys-patch", VERSION_WITH_HASH); - auto list = new tsl::elm::List(); + auto list = new tsl::elm::List(); list->addItem(new tsl::elm::CategoryHeader("FS - 0100000000000000")); list->addItem(config_noacidsigchk1.create_list_item("noacidsigchk_1.0.0-9.2.0")); list->addItem(config_noacidsigchk2.create_list_item("noacidsigchk_1.0.0-9.2.0")); - list->addItem(config_noncasigchk1.create_list_item("noncasigchk_10.0.0-16.1.0")); - list->addItem(config_noncasigchk2.create_list_item("noncasigchk_17.0.0+")); - list->addItem(config_nocntchk1.create_list_item("nocntchk_10.0.0-18.1.0")); - list->addItem(config_nocntchk2.create_list_item("nocntchk_19.0.0-20.5.0")); - list->addItem(config_nocntchk3.create_list_item("nocntchk_21.0.0+")); + list->addItem(config_noncasigchk1 .create_list_item("noncasigchk_1.0.0-3.0.2")); + list->addItem(config_noncasigchk2 .create_list_item("noncasigchk_4.0.0-16.1.0")); + list->addItem(config_noncasigchk3 .create_list_item("noncasigchk_17.0.0+")); + list->addItem(config_nocntchk1 .create_list_item("nocntchk_1.0.0-18.1.0")); + list->addItem(config_nocntchk2 .create_list_item("nocntchk_19.0.0+")); list->addItem(new tsl::elm::CategoryHeader("LDR - 0100000000000001")); - list->addItem(config_noacidsigchk3.create_list_item("noacidsigchk_10.0.0+")); + list->addItem(config_noacidsigchk4.create_list_item("noacidsigchk_10.0.0+")); list->addItem(new tsl::elm::CategoryHeader("ERPT - 010000000000002B")); - list->addItem(config_no_erpt.create_list_item("no_erpt")); + list->addItem(config_no_erpt .create_list_item("no_erpt")); list->addItem(new tsl::elm::CategoryHeader("ES - 0100000000000033")); - list->addItem(config_es1.create_list_item("es_1.0.0-8.1.1")); - list->addItem(config_es2.create_list_item("es_9.0.0-11.0.1")); - list->addItem(config_es3.create_list_item("es_12.0.0-18.1.0")); - list->addItem(config_es4.create_list_item("es_19.0.0+")); + list->addItem(config_es1 .create_list_item("es_1.0.0-8.1.1")); + list->addItem(config_es2 .create_list_item("es_9.0.0-11.0.1")); + list->addItem(config_es3 .create_list_item("es_12.0.0-18.1.0")); + list->addItem(config_es4 .create_list_item("es_19.0.0+")); list->addItem(new tsl::elm::CategoryHeader("OLSC - 010000000000003E")); - list->addItem(config_olsc1.create_list_item("olsc_6.0.0-14.1.2")); - list->addItem(config_olsc2.create_list_item("olsc_15.0.0-18.1.0")); - list->addItem(config_olsc3.create_list_item("olsc_19.0.0+")); + list->addItem(config_olsc1 .create_list_item("olsc_6.0.0-14.1.2")); + list->addItem(config_olsc2 .create_list_item("olsc_15.0.0-18.1.0")); + list->addItem(config_olsc3 .create_list_item("olsc_19.0.0+")); list->addItem(new tsl::elm::CategoryHeader("NIFM - 010000000000000F")); - list->addItem(config_ctest1.create_list_item("ctest_1.0.0-19.0.1")); - list->addItem(config_ctest2.create_list_item("ctest_20.0.0+")); + list->addItem(config_ctest1 .create_list_item("ctest_1.0.0-19.0.1")); + list->addItem(config_ctest2 .create_list_item("ctest_20.0.0+")); list->addItem(new tsl::elm::CategoryHeader("NIM - 0100000000000025")); - list->addItem(config_nim1.create_list_item("blankcal0crashfix_17.0.0+")); - list->addItem(config_nim_fw1.create_list_item("blockfirmwareupdates_1.0.0-5.1.0")); - list->addItem(config_nim_fw2.create_list_item("blockfirmwareupdates_6.0.0-6.2.0")); - list->addItem(config_nim_fw3.create_list_item("blockfirmwareupdates_7.0.0-11.0.1")); - list->addItem(config_nim_fw4.create_list_item("blockfirmwareupdates_12.0.0+")); + list->addItem(config_nim1 .create_list_item("blankcal0crashfix_17.0.0+")); + list->addItem(config_nim_fw1 .create_list_item("blockfirmwareupdates_1.0.0-5.1.0")); + list->addItem(config_nim_fw2 .create_list_item("blockfirmwareupdates_6.0.0-6.2.0")); + list->addItem(config_nim_fw3 .create_list_item("blockfirmwareupdates_7.0.0-11.0.1")); + list->addItem(config_nim_fw4 .create_list_item("blockfirmwareupdates_12.0.0+")); frame->setContent(list); return frame; } - ConfigEntry config_noacidsigchk1{"fs", "noacidsigchk_1.0.0-9.2.0", true}; - ConfigEntry config_noacidsigchk2{"fs", "noacidsigchk_1.0.0-9.2.0", true}; - ConfigEntry config_noncasigchk1{"fs", "noncasigchk_10.0.0-16.1.0", true}; - ConfigEntry config_noncasigchk2{"fs", "noncasigchk_17.0.0+", true}; - ConfigEntry config_nocntchk1{"fs", "nocntchk_10.0.0-18.1.0", true}; - ConfigEntry config_nocntchk2{"fs", "nocntchk_19.0.0-20.5.0", true}; - ConfigEntry config_nocntchk3{"fs", "nocntchk_21.0.0+", true}; - ConfigEntry config_noacidsigchk3{"ldr", "noacidsigchk_10.0.0+", true}; - ConfigEntry config_no_erpt{"erpt", "no_erpt", true}; - ConfigEntry config_es1{"es", "es_1.0.0-8.1.1", true}; - ConfigEntry config_es2{"es", "es_9.0.0-11.0.1", true}; - ConfigEntry config_es3{"es", "es_12.0.0-18.1.0", true}; - ConfigEntry config_es4{"es", "es_19.0.0+", true}; - ConfigEntry config_olsc1{"olsc", "olsc_6.0.0-14.1.2", true}; - ConfigEntry config_olsc2{"olsc", "olsc_15.0.0-18.1.0", true}; - ConfigEntry config_olsc3{"olsc", "olsc_19.0.0+", true}; - ConfigEntry config_ctest1{"nifm", "ctest_1.0.0-19.0.1", true}; - ConfigEntry config_ctest2{"nifm", "ctest_20.0.0+", true}; - ConfigEntry config_nim1{"nim", "blankcal0crashfix_17.0.0+", true}; - ConfigEntry config_nim_fw1{"nim", "blockfirmwareupdates_1.0.0-5.1.0", true}; - ConfigEntry config_nim_fw2{"nim", "blockfirmwareupdates_6.0.0-6.2.0", true}; - ConfigEntry config_nim_fw3{"nim", "blockfirmwareupdates_7.0.0-11.0.1", true}; - ConfigEntry config_nim_fw4{"nim", "blockfirmwareupdates_12.0.0+", true}; +private: + ConfigEntry config_noacidsigchk1{"fs", "noacidsigchk_1.0.0-9.2.0", true}; + ConfigEntry config_noacidsigchk2{"fs", "noacidsigchk_1.0.0-9.2.0", true}; + ConfigEntry config_noncasigchk1 {"fs", "noncasigchk_1.0.0-3.0.2", true}; + ConfigEntry config_noncasigchk2 {"fs", "noncasigchk_4.0.0-16.1.0", true}; + ConfigEntry config_noncasigchk3 {"fs", "noncasigchk_17.0.0+", true}; + ConfigEntry config_nocntchk1 {"fs", "nocntchk_1.0.0-18.1.0", true}; + ConfigEntry config_nocntchk2 {"fs", "nocntchk_19.0.0+", true}; + ConfigEntry config_noacidsigchk4{"ldr", "noacidsigchk_10.0.0+", true}; + ConfigEntry config_no_erpt {"erpt", "no_erpt", true}; + ConfigEntry config_es1 {"es", "es_1.0.0-8.1.1", true}; + ConfigEntry config_es2 {"es", "es_9.0.0-11.0.1", true}; + ConfigEntry config_es3 {"es", "es_12.0.0-18.1.0", true}; + ConfigEntry config_es4 {"es", "es_19.0.0+", true}; + ConfigEntry config_olsc1 {"olsc", "olsc_6.0.0-14.1.2", true}; + ConfigEntry config_olsc2 {"olsc", "olsc_15.0.0-18.1.0", true}; + ConfigEntry config_olsc3 {"olsc", "olsc_19.0.0+", true}; + ConfigEntry config_ctest1 {"nifm", "ctest_1.0.0-19.0.1", true}; + ConfigEntry config_ctest2 {"nifm", "ctest_20.0.0+", true}; + ConfigEntry config_nim1 {"nim", "blankcal0crashfix_17.0.0+", true}; + ConfigEntry config_nim_fw1 {"nim", "blockfirmwareupdates_1.0.0-5.1.0", true}; + ConfigEntry config_nim_fw2 {"nim", "blockfirmwareupdates_6.0.0-6.2.0", true}; + ConfigEntry config_nim_fw3 {"nim", "blockfirmwareupdates_7.0.0-11.0.1", true}; + ConfigEntry config_nim_fw4 {"nim", "blockfirmwareupdates_12.0.0+", true}; }; +// ── GuiLog ─────────────────────────────────────────────────────────────────── + +/** + * Displays the log INI file. + * + * libtesla version used ini_browse() (minIni callback iterator). + * libultrahand version uses getParsedDataFromIniFile() which returns an + * ordered map> – we iterate it directly. + * + * NOTE: libultrahand's INI parser uses std::map (alphabetical) unless the + * implementation preserves insertion order. If section/key ordering matters + * you may want to call ult::getOrderedParsedDataFromIniFile() if that API + * is available in your version of libultrahand, or fall back to a line-by-line + * reader. For a log display this ordering is generally acceptable. + */ class GuiLog final : public tsl::Gui { public: - GuiLog() { } - tsl::elm::Element* createUI() override { auto frame = new tsl::elm::OverlayFrame("sys-patch", VERSION_WITH_HASH); - auto list = new tsl::elm::List(); + auto list = new tsl::elm::List(); - if (does_file_exist(LOG_PATH)) { - struct CallbackUser { - tsl::elm::List* list; - std::string last_section; - } callback_userdata{list}; + // ult::isFile() replaces the old does_file_exist() helper that opened + // a raw FsFileSystem handle. + if (isFile(LOG_PATH)) { + const auto iniData = getParsedDataFromIniFile(LOG_PATH); - ini_browse([](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData){ - auto user = (CallbackUser*)UserData; - std::string_view value{Value}; + for (const auto& [sectionName, sectionMap] : iniData) { + list->addItem(new tsl::elm::CategoryHeader("Log: " + sectionName)); - if (value == "Skipped") { - return 1; - } + for (const auto& [key, value] : sectionMap) { + std::string_view sv{value}; - if (user->last_section != Section) { - user->last_section = Section; - user->list->addItem(new tsl::elm::CategoryHeader("Log: " + user->last_section)); - } - - #define F(x) ((x) >> 4) // 8bit -> 4bit - constexpr tsl::Color colour_syspatch{F(0), F(255), F(200), F(255)}; - constexpr tsl::Color colour_file{F(255), F(177), F(66), F(255)}; - constexpr tsl::Color colour_unpatched{F(250), F(90), F(58), F(255)}; - #undef F + if (sv == "Skipped") continue; - if (value.starts_with("Patched")) { - if (value.ends_with("(sys-patch)")) { - user->list->addItem(new tsl::elm::ListItem(Key, "Patched", colour_syspatch)); + if (sv.starts_with("Patched")) { + list->addItem(new tsl::elm::ListItem(key, "Patched", true)); + } else if (sv.starts_with("Unpatched") || sv.starts_with("Disabled")) { + list->addItem(new tsl::elm::ListItem(key, value, true)); } else { - user->list->addItem(new tsl::elm::ListItem(Key, "Patched", colour_file)); + // stats section or anything else + list->addItem(new tsl::elm::ListItem(key, value, true)); } - } else if (value.starts_with("Unpatched") || value.starts_with("Disabled")) { - user->list->addItem(new tsl::elm::ListItem(Key, Value, colour_unpatched)); - } else if (user->last_section == "stats") { - user->list->addItem(new tsl::elm::ListItem(Key, Value, tsl::style::color::ColorDescription)); - } else { - user->list->addItem(new tsl::elm::ListItem(Key, Value, tsl::style::color::ColorText)); } - - return 1; - }, &callback_userdata, LOG_PATH); + } } else { list->addItem(new tsl::elm::ListItem("No log found!")); } @@ -222,17 +238,15 @@ class GuiLog final : public tsl::Gui { } }; +// ── GuiMain ────────────────────────────────────────────────────────────────── class GuiMain final : public tsl::Gui { public: - GuiMain() { } - tsl::elm::Element* createUI() override { - auto frame = new tsl::elm::OverlayFrame("sys-patch", VERSION_WITH_HASH); - auto list = new tsl::elm::List(); - + auto frame = new tsl::elm::OverlayFrame("sys-patch", VERSION_WITH_HASH); + auto list = new tsl::elm::List(); auto options = new tsl::elm::ListItem("Options"); - auto toggle = new tsl::elm::ListItem("Toggle patches"); - auto log = new tsl::elm::ListItem("Log"); + auto toggle = new tsl::elm::ListItem("Toggle patches"); + auto log = new tsl::elm::ListItem("Log"); options->setClickListener([](u64 keys) -> bool { if (keys & HidNpadButton_A) { @@ -268,7 +282,7 @@ class GuiMain final : public tsl::Gui { } }; -// libtesla already initialized fs, hid, pl, pmdmnt, hid:sys and set:sys +// ── Overlay ─────────────────────────────────────────────────────────────────── class SysPatchOverlay final : public tsl::Overlay { public: std::unique_ptr loadInitialGui() override { @@ -278,8 +292,11 @@ class SysPatchOverlay final : public tsl::Overlay { } // namespace -int main(int argc, char **argv) { - create_dir("/config/"); - create_dir("/config/sys-patch/"); +// ── main ────────────────────────────────────────────────────────────────────── +int main(int argc, char** argv) { + // ult::createDirectory() replaces the old create_dir() helper that used raw + // FsFileSystem calls. It is a no-op when the directory already exists. + ensureDir("sdmc:/config/"); + ensureDir("sdmc:/config/sys-patch/"); return tsl::loop(argc, argv); } diff --git a/sysmod/Makefile b/sysmod/Makefile index 2663479..536873f 100644 --- a/sysmod/Makefile +++ b/sysmod/Makefile @@ -56,6 +56,7 @@ CFLAGS := -g -Wall -O2 -ffunction-sections \ CFLAGS += $(INCLUDE) -D__SWITCH__ CXXFLAGS := $(CFLAGS) -std=c++23 -fno-rtti -fno-exceptions +CXXFLAGS += $(EXTRA_FLAGS) ASFLAGS := -g $(ARCH) LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) diff --git a/sysmod/src/main.cpp b/sysmod/src/main.cpp index a542164..ba38a76 100644 --- a/sysmod/src/main.cpp +++ b/sysmod/src/main.cpp @@ -20,56 +20,99 @@ u8 AMS_KEYGEN{}; // set on startup u64 AMS_HASH{}; // set on startup bool VERSION_SKIP{}; // set on startup -struct DebugEventInfo { - u32 event_type; - u32 flags; - u64 thread_id; - u64 title_id; - u64 process_id; - char process_name[12]; - u32 mmu_flags; - u8 _0x30[0x10]; -}; - template -constexpr void str2hex(const char* s, T* data, u8& size) { - // skip leading 0x (if any) - if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) { - s += 2; +constexpr void hex_to_bytes(const char* s, T* data, u8& size) { + if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) s += 2; + + constexpr auto nibble = [](char c) -> u8 { + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= '0' && c <= '9') return c - '0'; + return 0; + }; + + while (*s) { + u8 high = nibble(*s++); + u8 low = nibble(*s++); + data[size++] = (high << 4) | low; } +} - // invalid string will cause a compile-time error due to no return - constexpr auto hexstr_2_nibble = [](char c) -> u8 { - if (c >= 'A' && c <= 'F') { return c - 'A' + 10; } - if (c >= 'a' && c <= 'f') { return c - 'a' + 10; } - if (c >= '0' && c <= '9') { return c - '0'; } +template +constexpr void pattern_to_bytes(const char* s, T* data, u8& size) { + if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) s += 2; + + constexpr auto nibble = [](char c) -> u8 { + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= '0' && c <= '9') return c - '0'; + return 0xFF; // invalid → will fail match }; - // parse and convert string - while (*s != '\0') { - if (sizeof(T) == sizeof(u16) && *s == '.') { - data[size] = REGEX_SKIP; - s++; + while (*s) { + if (*s == '.') { + u8 dot_count = 0; + while (s[dot_count] == '.') ++dot_count; + + u8 wildcards = dot_count / 2; + + for (u8 i = 0; i < wildcards; ++i) { + data[size++] = REGEX_SKIP; + } + + s += dot_count; + } else { - data[size] |= hexstr_2_nibble(*s++) << 4; - data[size] |= hexstr_2_nibble(*s++) << 0; + u16 value = 0; + bool is_wild_high = false; + bool is_wild_low = false; + + // High nibble + if (*s == '?') { + is_wild_high = true; + ++s; + } else { + value |= nibble(*s++) << 8; + } + + // Low nibble + if (*s == '?') { + is_wild_low = true; + ++s; + } else { + value |= nibble(*s++); + } + + if (is_wild_high && is_wild_low) { + // ?? → full byte wildcard (same as ..) + data[size++] = REGEX_SKIP; + } else if (is_wild_high || is_wild_low) { + // Partial match: store value with high bit set to indicate mask needed + // We'll use: if value >= 0x100 → it's a masked byte + // value & 0xFF = actual byte, value >> 8 = mask (0xF0 or 0x0F) + u8 actual = value & 0xFF; + u8 mask = is_wild_high ? 0x0F : 0xF0; + data[size++] = actual | (static_cast(mask) << 8); + } else { + // Exact byte + data[size++] = value; + } } - size++; } } struct PatternData { constexpr PatternData(const char* s) { - str2hex(s, data, size); + pattern_to_bytes(s, data, size); } - u16 data[60]{}; // reasonable max pattern length, adjust as needed + u16 data[60]{}; u8 size{}; }; struct PatchData { constexpr PatchData(const char* s) { - str2hex(s, data, size); + hex_to_bytes(s, data, size); } template @@ -84,7 +127,7 @@ struct PatchData { return !std::memcmp(data, _data, size); } - u8 data[20]{}; // reasonable max patch length, adjust as needed + u8 data[20]{}; u8 size{}; }; @@ -235,14 +278,40 @@ constexpr auto ctest_applied(const u8* data, u32 inst) -> bool { return ctest_patch(inst).cmp(data); } +// Note: Patterns can compose of byte wildcards represented as ".." or "??". Patterns can also consist of high, or low nibble wildcarding, represented, with the example being wildcarded being "A9" as "A?" or "?9". +// example 1: just byte wildcarding: +// C8FE4739 -> C8....39 = 2 bytes wildcarded +// C8FE4739 -> C8????39 = 2 bytes wildcarded +// C8FE4739 -> C?F?4??9 = 4 nibbles wildcarded +// nibble wildcarding must be done with "?", and must not be mixed with ".", ".." should be used when wildcarding an entire byte, or "??", but not a mix of "?." or ".?" +// a pattern can contain both "..", "??", or nibble wildcarding, as long as one does not mix "?." or "?." +// patterns should be optimized in such a manner that they yield only one result. +// patterns might yield results for more firmware versions, but if it yields more than one result (per firmware version), it should be condensed to near similar versions instead which only yields one result. +// a pattern should not contain the bytes being patched, they should be wildcarded. +// if the bytes being patched align with the patch partially, then the partial bytes can be in the pattern, the same applies to if the pattern contains the length of the patch. +// the bytes being tested are defined by the _cond, and does not need to be in the pattern, and shouldn't be in the pattern, if the bytes being tested are also the bytes being patched. +// () indicate testing, {} indicate what is being patched +// example: +// "0x00....0240F9........E8", 6, 0, +// the bytes being tested, and patch size is the same, 6 from start of pattern, then patch 0 from start of where the test was designated: +// "0x00....0240F9{(........)}E8" +// if moving the head from what is being tested, the bytes, if in pattern, should be wildcarded by the length of the patch being applied +// "0x00....0240F9........E8C8FE4739", 6, 4, +// example {} should be wildcarded, as those are the bytes being patched, the bytes being tested can in that context contain bytes in the pattern: +// "0x00....0240F9(......94){E8C8FE47}39", 6, 4, +// example with wildcarding: +// "0x00....0240F9(......94){........}39", 6, 4, +// +// designing new patterns should ideally conform to specification above. + constinit Patterns fs_patterns[] = { { "noacidsigchk_1.0.0-9.2.0", "0xC8FE4739", -24, 0, bl_cond, ret0_patch, ret0_applied, true, FW_VER_ANY, MAKEHOSVERSION(9,2,0) }, // moved to loader 10.0.0 { "noacidsigchk_1.0.0-9.2.0", "0x0210911F000072", -5, 0, bl_cond, ret0_patch, ret0_applied, true, FW_VER_ANY, MAKEHOSVERSION(9,2,0) }, // moved to loader 10.0.0 - { "noncasigchk_10.0.0-16.1.0", "0x1E48391F.0071..0054", -17, 0, tbz_cond, nop_patch, nop_applied, true, MAKEHOSVERSION(10,0,0), MAKEHOSVERSION(16,1,0) }, - { "noncasigchk_17.0.0+", "0x0694..00.42.0091", -18, 0, tbz_cond, nop_patch, nop_applied, true, MAKEHOSVERSION(17,0,0), FW_VER_ANY }, - { "nocntchk_10.0.0-18.1.0", "0x00..0240F9....08.....00...00...0037", 6, 0, bl_cond, ret0_patch, ret0_applied, true, MAKEHOSVERSION(10,0,0), MAKEHOSVERSION(18,1,0) }, - { "nocntchk_19.0.0-20.5.0", "0x00..0240F9....08.....00...00...0054", 6, 0, bl_cond, ret0_patch, ret0_applied, true, MAKEHOSVERSION(19,0,0), MAKEHOSVERSION(20,5,0) }, - { "nocntchk_21.0.0+", "0x00..0240F9....E8.....00...00...0054", 6, 0, bl_cond, ret0_patch, ret0_applied, true, MAKEHOSVERSION(21,0,0), FW_VER_ANY }, + { "noncasigchk_1.0.0-3.0.2", "0x88..42..58", -4, 0, tbz_cond, nop_patch, nop_applied, true, MAKEHOSVERSION(1,0,0), MAKEHOSVERSION(3,0,2) }, + { "noncasigchk_4.0.0-16.1.0", "0x1E4839....00......0054", -17, 0, tbz_cond, nop_patch, nop_applied, true, MAKEHOSVERSION(4,0,0), MAKEHOSVERSION(16,1,0) }, + { "noncasigchk_17.0.0+", "0x0694....00..42..0091", -18, 0, tbz_cond, nop_patch, nop_applied, true, MAKEHOSVERSION(17,0,0), FW_VER_ANY }, + { "nocntchk_1.0.0-18.1.0", "0x40F9........081C00121F05", 2, 0, bl_cond, ret0_patch, ret0_applied, true, MAKEHOSVERSION(1,0,0), MAKEHOSVERSION(18,1,0) }, + { "nocntchk_19.0.0+", "0x40F9............40B9091C", 2, 0, bl_cond, ret0_patch, ret0_applied, true, MAKEHOSVERSION(19,0,0), FW_VER_ANY }, }; constinit Patterns ldr_patterns[] = { @@ -250,33 +319,34 @@ constinit Patterns ldr_patterns[] = { }; constinit Patterns erpt_patterns[] = { - { "no_erpt", "0x...D1FD7B02A9FD830091F76305A9", 0, 0, sub_cond, mov0_ret_patch, mov0_ret_applied, true, FW_VER_ANY }, // FF4305D1 - sub sp, sp, #0x150 patched to E0031F2AC0035FD6 - mov w0, wzr, ret + { "no_erpt", "0xFD7B02A9FD830091F76305A9", -4, 0, sub_cond, mov0_ret_patch, mov0_ret_applied, true, FW_VER_ANY }, // FF4305D1 - sub sp, sp, #0x150 patched to E0031F2AC0035FD6 - mov w0, wzr, ret }; constinit Patterns es_patterns[] = { - { "es_1.0.0-8.1.1", "0x....E8.00...FF97.0300AA..00.....E0.0091..0094.7E4092.......A9", 36, 0, es_cond, mov0_patch, mov0_applied, true, MAKEHOSVERSION(1,0,0), MAKEHOSVERSION(8,1,1) }, - { "es_9.0.0-11.0.1", "0x00...............00.....A0..D1...97.......A9", 30, 0, es_cond, mov0_patch, mov0_applied, true, MAKEHOSVERSION(9,0,0), MAKEHOSVERSION(11,0,1) }, - { "es_12.0.0-18.1.0", "0x02.00...........00...00.....A0..D1...97.......A9", 32, 0, es_cond, mov0_patch, mov0_applied, true, MAKEHOSVERSION(12,0,0), MAKEHOSVERSION(18,1,0) }, - { "es_19.0.0+", "0xA1.00...........00...00.....A0..D1...97.......A9", 32, 0, es_cond, mov0_patch, mov0_applied, true, MAKEHOSVERSION(19,0,0), FW_VER_ANY }, + { "es_1.0.0-8.1.1", "0x0091....0094..7E4092", 10, 0, es_cond, mov0_patch, mov0_applied, true, MAKEHOSVERSION(1,0,0), MAKEHOSVERSION(8,1,1) }, + { "es_9.0.0-11.0.1", "0x00..........A0....D1....FF97", 14, 0, es_cond, mov0_patch, mov0_applied, true, MAKEHOSVERSION(9,0,0), MAKEHOSVERSION(11,0,1) }, + { "es_12.0.0-18.1.0", "0x02........D2..52....0091", 32, 0, es_cond, mov0_patch, mov0_applied, true, MAKEHOSVERSION(12,0,0), MAKEHOSVERSION(18,1,0) }, + { "es_19.0.0+", "0xA1........031F2A....0091", 32, 0, es_cond, mov0_patch, mov0_applied, true, MAKEHOSVERSION(19,0,0), FW_VER_ANY }, }; constinit Patterns olsc_patterns[] = { - { "olsc_6.0.0-14.1.2", "0x00.73..F968024039..00...00", 42, 0, bl_cond, ret1_patch, ret1_applied, true, MAKEHOSVERSION(6,0,0), MAKEHOSVERSION(14,1,2) }, - { "olsc_15.0.0-18.1.0", "0x00.73..F968024039..00...00", 38, 0, bl_cond, ret1_patch, ret1_applied, true, MAKEHOSVERSION(15,0,0), MAKEHOSVERSION(18,1,0) }, - { "olsc_19.0.0+", "0x00.73..F968024039..00...00", 42, 0, bl_cond, ret1_patch, ret1_applied, true, MAKEHOSVERSION(19,0,0), FW_VER_ANY }, + { "olsc_6.0.0-14.1.2", "0x00..73....F9....4039", 42, 0, bl_cond, ret1_patch, ret1_applied, true, MAKEHOSVERSION(6,0,0), MAKEHOSVERSION(14,1,2) }, + { "olsc_15.0.0-18.1.0", "0x00..73....F9....4039", 38, 0, bl_cond, ret1_patch, ret1_applied, true, MAKEHOSVERSION(15,0,0), MAKEHOSVERSION(18,1,0) }, + { "olsc_19.0.0+", "0x00..73....F9....4039", 42, 0, bl_cond, ret1_patch, ret1_applied, true, MAKEHOSVERSION(19,0,0), FW_VER_ANY }, }; constinit Patterns nifm_patterns[] = { - { "ctest_1.0.0-19.0.1", "0x03.AAE003.AA...39..04F8....E0", -29, 0, ctest_cond, ctest_patch, ctest_applied, true, FW_VER_ANY, MAKEHOSVERSION(18,1,0) }, - { "ctest_20.0.0+", "0x03.AA...AA.........0314AA..14AA", -17, 0, ctest_cond, ctest_patch, ctest_applied, true, MAKEHOSVERSION(20,0,0), FW_VER_ANY }, + { "ctest_1.0.0-19.0.1", "0x03..AAE003..AA......39....04F8........E0", -29, 0, ctest_cond, ctest_patch, ctest_applied, true, FW_VER_ANY, MAKEHOSVERSION(19,0,1) }, + { "ctest_20.0.0+", "0x03..AA......AA..................0314AA....14AA", -17, 0, ctest_cond, ctest_patch, ctest_applied, true, MAKEHOSVERSION(20,0,0), FW_VER_ANY }, }; constinit Patterns nim_patterns[] = { - { "blankcal0crashfix_17.0.0+", "0x00351F2003D5...............97..0094..00.....61", 6, 0, adr_cond, mov2_patch, mov2_applied, true, MAKEHOSVERSION(17,0,0), FW_VER_ANY }, - { "blockfirmwareupdates_1.0.0-5.1.0", "0x1139F30301AA81.40F9E0.1191", -30, 0, block_fw_updates_cond, mov0_ret_patch, mov0_ret_applied, true, MAKEHOSVERSION(1,0,0), MAKEHOSVERSION(5,1,0) }, - { "blockfirmwareupdates_6.0.0-6.2.0", "0xF30301AA.4E40F9E0..91", -40, 0, block_fw_updates_cond, mov0_ret_patch, mov0_ret_applied, true, MAKEHOSVERSION(6,0,0), MAKEHOSVERSION(6,2,0) }, - { "blockfirmwareupdates_7.0.0-11.0.1", "0xF30301AA014C40F9F40300AAE0..91", -36, 0, block_fw_updates_cond, mov0_ret_patch, mov0_ret_applied, true, MAKEHOSVERSION(7,0,0), MAKEHOSVERSION(11,0,1) }, - { "blockfirmwareupdates_12.0.0+", "0x280841F9084C00F9E0031F.C0035FD6", 16, 0, block_fw_updates_cond, mov0_ret_patch, mov0_ret_applied, true, MAKEHOSVERSION(12,0,0), FW_VER_ANY }, + { "blankcal0crashfix_17.0.0+", "0x00351F2003D5..............................97....0094....00..........61", 6, 0, adr_cond, mov2_patch, mov2_applied, true, MAKEHOSVERSION(17,0,0), FW_VER_ANY }, + { "blockfirmwareupdates_1.0.0-5.1.0", "0x1139F3", -30, 0, block_fw_updates_cond, mov0_ret_patch, mov0_ret_applied, true, MAKEHOSVERSION(1,0,0), MAKEHOSVERSION(5,1,0) }, + { "blockfirmwareupdates_6.0.0-6.2.0", "0xF30301AA..4E", -40, 0, block_fw_updates_cond, mov0_ret_patch, mov0_ret_applied, true, MAKEHOSVERSION(6,0,0), MAKEHOSVERSION(6,2,0) }, + { "blockfirmwareupdates_7.0.0-10.2.0", "0xF30301AA014C", -36, 0, block_fw_updates_cond, mov0_ret_patch, mov0_ret_applied, true, MAKEHOSVERSION(7,0,0), MAKEHOSVERSION(10,2,0) }, + { "blockfirmwareupdates_11.0.0-11.0.1", "0x9AF0....................C0035FD6", 16, 0, block_fw_updates_cond, mov0_ret_patch, mov0_ret_applied, true, MAKEHOSVERSION(11,0,0), MAKEHOSVERSION(11,0,1) }, + { "blockfirmwareupdates_12.0.0+", "0x41....4C............C0035FD6", 14, 0, block_fw_updates_cond, mov0_ret_patch, mov0_ret_applied, true, MAKEHOSVERSION(12,0,0), FW_VER_ANY }, }; // NOTE: add system titles that you want to be patched to this table. @@ -336,7 +406,9 @@ void patcher(Handle handle, const u8* data, size_t data_size, u64 addr, std::spa continue; } - for (u32 i = 0; i < data_size; i++) { + // Try to find and apply this pattern + bool found = false; + for (u32 i = 0; i < data_size && !found; i++) { if (i + p.byte_pattern.size >= data_size) { break; } @@ -345,8 +417,22 @@ void patcher(Handle handle, const u8* data, size_t data_size, u64 addr, std::spa // skipping over any bytes if the value is REGEX_SKIP u32 count{}; while (count < p.byte_pattern.size) { - if (p.byte_pattern.data[count] != data[i + count] && p.byte_pattern.data[count] != REGEX_SKIP) { - break; + u16 pattern_entry = p.byte_pattern.data[count]; + u8 memory_byte = data[i + count]; + + if (pattern_entry == REGEX_SKIP) { + // full wildcard — always matches + } else if (pattern_entry > 0xFF) { + // masked nibble match + u8 expected = pattern_entry & 0xFF; + u8 mask = pattern_entry >> 8; + if ((memory_byte & mask) != (expected & mask)) { + break; + } + } else { + if (memory_byte != pattern_entry) { + break; + } } count++; } @@ -369,92 +455,130 @@ void patcher(Handle handle, const u8* data, size_t data_size, u64 addr, std::spa } else { p.result = PatchResult::PATCHED_SYSPATCH; } - // move onto next pattern - break; + found = true; } else if (p.applied(data + inst_offset + p.patch_offset, inst)) { // patch already applied by sigpatches p.result = PatchResult::PATCHED_FILE; - break; + found = true; } } } } } -auto apply_patch(PatchEntry& patch) -> bool { - Handle handle{}; - DebugEventInfo event_info{}; +// Check if a patch entry is valid for the current firmware version +auto is_patch_version_valid(const PatchEntry& patch) -> bool { + if (!VERSION_SKIP) { + return true; + } + return !(patch.min_fw_ver && patch.min_fw_ver > FW_VERSION) && + !(patch.max_fw_ver && patch.max_fw_ver < FW_VERSION); +} + + + +// Find and open the process with the given title_id +auto find_process_by_title_id(u64 title_id, Handle& out_handle) -> bool { u64 pids[0x50]{}; s32 process_count{}; - constexpr u64 overlap_size = 0x4f; - static u8 buffer[READ_BUFFER_SIZE + overlap_size]; - std::memset(buffer, 0, sizeof(buffer)); + if (R_FAILED(svcGetProcessList(&process_count, pids, 0x50))) { + return false; + } - // skip if version isn't valid - if (VERSION_SKIP && - ((patch.min_fw_ver && patch.min_fw_ver > FW_VERSION) || - (patch.max_fw_ver && patch.max_fw_ver < FW_VERSION))) { - for (auto& p : patch.patterns) { - p.result = PatchResult::SKIPPED; + for (s32 i = 0; i < process_count; i++) { + Handle handle{}; + #ifdef USE_DEBUG_EVENT + DebugEvent event{}; + #else + DebugEventInfo event{}; + #endif + + if (R_FAILED(svcDebugActiveProcess(&handle, pids[i]))) { + continue; } - return true; - } - if (R_FAILED(svcGetProcessList(&process_count, pids, 0x50))) { - return false; + if (R_SUCCEEDED(svcGetDebugEvent(&event, handle))) { + if (event.type == DebugEventType_CreateProcess && + event.info.create_process.program_id == title_id) { + out_handle = handle; + return true; + } + } + + svcCloseHandle(handle); } - for (s32 i = 0; i < (process_count - 1); i++) { - if (R_SUCCEEDED(svcDebugActiveProcess(&handle, pids[i])) && - R_SUCCEEDED(svcGetDebugEvent(&event_info, handle)) && - patch.title_id == event_info.title_id) { - MemoryInfo mem_info{}; - u64 addr{}; - u32 page_info{}; - - for (;;) { - if (R_FAILED(svcQueryDebugProcessMemory(&mem_info, &page_info, handle, addr))) { - break; - } - addr = mem_info.addr + mem_info.size; + return false; +} - // if addr=0 then we hit the reserved memory section - if (!addr) { - break; - } - // skip memory that we don't want - if (!mem_info.size || (mem_info.perm & Perm_Rx) != Perm_Rx || ((mem_info.type & 0xFF) != MemType_CodeStatic)) { - continue; - } +// Patch all executable memory regions in a process +auto patch_process_memory(Handle handle, const PatchEntry& patch, u8* buffer, u64 overlap_size) -> void { + MemoryInfo mem_info{}; + u64 addr{}; + u32 page_info{}; - for (u64 sz = 0; sz < mem_info.size; sz += READ_BUFFER_SIZE - overlap_size) { - const auto actual_size = std::min(READ_BUFFER_SIZE, mem_info.size - sz); - if (R_FAILED(svcReadDebugProcessMemory(buffer + overlap_size, handle, mem_info.addr + sz, actual_size))) { - break; - } else { - patcher(handle, buffer, actual_size + overlap_size, mem_info.addr + sz - overlap_size, patch.patterns); - if (actual_size >= overlap_size) { - memcpy(buffer, buffer + READ_BUFFER_SIZE, overlap_size); - std::memset(buffer + overlap_size, 0, READ_BUFFER_SIZE); - } else { - const auto bytes_to_overlap = std::min(overlap_size, actual_size); - memcpy(buffer, buffer + READ_BUFFER_SIZE + (actual_size - bytes_to_overlap), bytes_to_overlap); - std::memset(buffer + bytes_to_overlap, 0, sizeof(buffer) - bytes_to_overlap); - } - } + for (;;) { + if (R_FAILED(svcQueryDebugProcessMemory(&mem_info, &page_info, handle, addr))) { + break; + } + addr = mem_info.addr + mem_info.size; + + // if addr=0 then we hit the reserved memory section + if (!addr) { + break; + } + // skip memory that we don't want + if (!mem_info.size || (mem_info.perm & Perm_Rx) != Perm_Rx || ((mem_info.type & 0xFF) != MemType_CodeStatic)) { + continue; + } + + // Process this memory region in chunks + for (u64 sz = 0; sz < mem_info.size; sz += READ_BUFFER_SIZE - overlap_size) { + const auto actual_size = std::min(READ_BUFFER_SIZE, mem_info.size - sz); + if (R_FAILED(svcReadDebugProcessMemory(buffer + overlap_size, handle, mem_info.addr + sz, actual_size))) { + break; + } else { + patcher(handle, buffer, actual_size + overlap_size, mem_info.addr + sz - overlap_size, patch.patterns); + + // Manage overlap buffer for next iteration + if (actual_size >= overlap_size) { + memcpy(buffer, buffer + READ_BUFFER_SIZE, overlap_size); + std::memset(buffer + overlap_size, 0, READ_BUFFER_SIZE); + } else { + const auto bytes_to_overlap = std::min(overlap_size, actual_size); + memcpy(buffer, buffer + READ_BUFFER_SIZE + (actual_size - bytes_to_overlap), bytes_to_overlap); + std::memset(buffer + bytes_to_overlap, 0, sizeof(buffer) - bytes_to_overlap); } } - svcCloseHandle(handle); - return true; - } else if (handle) { - svcCloseHandle(handle); - handle = 0; } } +} - return false; +auto apply_patch(PatchEntry& patch) -> bool { + // skip if version isn't valid + if (!is_patch_version_valid(patch)) { + for (auto& p : patch.patterns) { + p.result = PatchResult::SKIPPED; + } + return true; + } + + Handle handle{}; + + if (!find_process_by_title_id(patch.title_id, handle)) { + return false; + } + + constexpr u64 overlap_size = 0x4f; + static u8 buffer[READ_BUFFER_SIZE + overlap_size]; + std::memset(buffer, 0, sizeof(buffer)); + + patch_process_memory(handle, patch, buffer, overlap_size); + + svcCloseHandle(handle); + return true; } // creates a directory, non-recursive!