diff --git a/.gitignore b/.gitignore index 96a91c3..3156dc4 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,7 @@ build *.nsp *.nso -*.npdm \ No newline at end of file +*.npdm +*.zip +sdout/ +.DS_Store \ No newline at end of file diff --git a/README.MD b/README.MD index 320bf66..90d07b4 100644 --- a/README.MD +++ b/README.MD @@ -31,6 +31,23 @@ You have two sections:
Order doesn’t matter.
Just add the program ID(s) under whichever environment you want them blocked in.
+### Firmware Version Conditions (Optional) + +Each program ID can optionally include a firmware version condition.
+When a condition is set, the program is **only blocked if the current firmware does NOT match the condition**.
+ +Supported operators: `=` (equal), `>=`, `<=`, `>`, `<`
+ +Format: ` = `
+ +Examples: +``` +[SysNand] +0100000000001000 ; No condition: always blocked in SysNAND +010000000000XXXX = 16.0.0 ; Blocked unless firmware == 16.0.0 +010000000000YYYY = >=17.0.0 ; Blocked unless firmware >= 17.0.0 +``` + ## Example For instance this config will block themes on ``SysNand``.
@@ -41,5 +58,13 @@ For instance this config will block themes on ``SysNand``.
[EmuNand] ``` +With firmware version conditions — only block a theme if firmware is not 16.0.0:
+``` +[SysNand] +0100000000001000 = 16.0.0 + +[EmuNand] +``` + ## Thanks to - Arcdelta for motivating me to actually finish this lmao. diff --git a/sysmodule/Makefile b/sysmodule/Makefile index e0fc730..5aa30ef 100644 --- a/sysmodule/Makefile +++ b/sysmodule/Makefile @@ -146,15 +146,29 @@ ifneq ($(ROMFS),) export NROFLAGS += --romfsdir=$(CURDIR)/$(ROMFS) endif -.PHONY: $(BUILD) clean all +.PHONY: $(BUILD) clean dist all + +TITLE_ID := 420000000000042A +SD_OUT := sdout #--------------------------------------------------------------------------------- -all: $(BUILD) +# default: build + package SD card layout +dist: $(BUILD) + @echo packaging SD layout ... + @mkdir -p $(SD_OUT)/atmosphere/contents/$(TITLE_ID)/flags + @mkdir -p $(SD_OUT)/config/sys-env + @cp $(OUTPUT).nsp $(SD_OUT)/atmosphere/contents/$(TITLE_ID)/exefs.nsp + @touch $(SD_OUT)/atmosphere/contents/$(TITLE_ID)/flags/boot2.flag + @cp config.ini.template $(SD_OUT)/config/sys-env/config.ini.example + @cd $(SD_OUT) && zip -r ../$(TARGET).zip . + @echo built $(TARGET).zip $(BUILD): @[ -d $@ ] || mkdir -p $@ @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile +all: dist + #--------------------------------------------------------------------------------- clean: @echo clean ... @@ -163,7 +177,7 @@ ifeq ($(strip $(APP_JSON)),) else @rm -fr $(BUILD) $(TARGET).nsp $(TARGET).nso $(TARGET).npdm $(TARGET).elf endif - + @rm -fr $(SD_OUT) $(TARGET).zip #--------------------------------------------------------------------------------- else diff --git a/sysmodule/config.ini.template b/sysmodule/config.ini.template new file mode 100644 index 0000000..4edaf1d --- /dev/null +++ b/sysmodule/config.ini.template @@ -0,0 +1,31 @@ +; +; sys-env configuration +; Path: sdmc:/config/sys-env/config.ini +; +; ========== Format ========== +; +; [SysNand] programs to block in SysNAND +; [EmuNand] programs to block in EmuNAND +; +; Each entry: <16-char program ID>[ = ] +; +; Without version condition: always blocked in this environment +; With version condition: blocked only when firmware does NOT match the condition +; +; ========== Version Operators ========== +; = 16.0.0 exact match (equal) +; >= 16.0.0 greater than or equal +; <= 16.0.0 less than or equal +; > 16.0.0 greater than +; < 16.0.0 less than +; +; ========== Examples ========== + +[SysNand] +;0100000000001000 ; Always block themes in SysNAND +;010000000000XXXX = 16.0.0 ; Block unless firmware is 16.0.0 +;010000000000YYYY = >=17.0.0 ; Block unless firmware >= 17.0.0 + +[EmuNand] +;0100000000001000 ; Always block themes in EmuNAND +;010000000000XXXX = 16.0.0 ; Block unless firmware is 16.0.0 diff --git a/sysmodule/source/fs.cpp b/sysmodule/source/fs.cpp index 032d3d9..3daa750 100644 --- a/sysmodule/source/fs.cpp +++ b/sysmodule/source/fs.cpp @@ -1,13 +1,12 @@ #include #include #include -#include #include #include -#include #include #include #include "result.hpp" +#include "fs.hpp" namespace fs { @@ -36,7 +35,22 @@ namespace fs { return str.compare(str.length() - suffix.length(), suffix.length(), suffix) == 0; } - Result EditContent(std::vector &matchList, std::string &env, std::string &del) { + bool CheckVersion(u32 currentVer, const ProgramEntry &entry) { + if (!entry.hasVersionCheck) { + return true; + } + + switch (entry.op) { + case OP_EQ: return currentVer == entry.version; + case OP_GE: return currentVer >= entry.version; + case OP_LE: return currentVer <= entry.version; + case OP_GT: return currentVer > entry.version; + case OP_LT: return currentVer < entry.version; + default: return false; + } + } + + Result EditContent(std::vector &matchList, std::string &env, std::string &del, u32 currentVer) { DIR *dir = opendir(CONTENTS); if (!dir) { return SYSENV_RC(SysEnvResult_OpenContentsFailed); @@ -60,11 +74,45 @@ namespace fs { modified.erase(modified.length() - del.length()); } - bool inList = std::find(matchList.begin(), matchList.end(), name) != matchList.end() || std::find(matchList.begin(), matchList.end(), modified) != matchList.end(); + // Find matching ProgramEntry + // Also try stripping the current env suffix to match previously blocked directories + std::string matchName = modified; + if (EndsWith(matchName, env)) { + matchName.erase(matchName.length() - env.length()); + } + + ProgramEntry *matched = nullptr; + for (auto &pe : matchList) { + if (name == pe.id || modified == pe.id || matchName == pe.id) { + matched = &pe; + break; + } + } + + if (matched != nullptr) { + bool shouldBlock; + if (matched->hasVersionCheck) { + // Has version condition: block only when firmware doesn't match + shouldBlock = !CheckVersion(currentVer, *matched); + } else { + // No version condition: always block (original behavior) + shouldBlock = true; + } - if (inList) { - if (!EndsWith(modified, env)) { - modified += env; + if (shouldBlock) { + if (!EndsWith(modified, env)) { + modified += env; + } + } else { + // Should not block: remove env suffix to restore loading + if (EndsWith(modified, env)) { + modified.erase(modified.length() - env.length()); + } + } + } else { + // Not in matchList: clean up orphaned .bak from removed entries + if (EndsWith(modified, env)) { + modified.erase(modified.length() - env.length()); } } @@ -96,7 +144,98 @@ namespace fs { return SYSENV_RC(SysEnvResult_HeaderMissing); } - void ReadConfigEntries(std::ifstream &file, std::vector &entries) { + // Parse the version condition part of a config line, e.g. "= >=16.0.0" or "= 16.0.0" + Result ParseVersionCondition(const std::string &condition, ProgramEntry &entry) { + std::string cond = condition; + + // Strip leading and trailing spaces + size_t pos = 0; + while (pos < cond.size() && cond[pos] == ' ') { + pos++; + } + cond = cond.substr(pos); + while (!cond.empty() && cond.back() == ' ') { + cond.pop_back(); + } + + if (cond.empty()) { + return SYSENV_RC(SysEnvResult_InvalidVersionFormat); + } + + // Detect operator + u8 op; + size_t verStart; + if (cond.compare(0, 2, ">=") == 0) { + op = OP_GE; + verStart = 2; + } else if (cond.compare(0, 2, "<=") == 0) { + op = OP_LE; + verStart = 2; + } else if (cond[0] == '>') { + op = OP_GT; + verStart = 1; + } else if (cond[0] == '<') { + op = OP_LT; + verStart = 1; + } else if (cond[0] == '=') { + op = OP_EQ; + verStart = 1; + } else { + // No operator: default to exact match (supports bare "16.0.0") + op = OP_EQ; + verStart = 0; + } + + std::string verStr = cond.substr(verStart); + // Strip spaces before version number + pos = 0; + while (pos < verStr.size() && verStr[pos] == ' ') { + pos++; + } + verStr = verStr.substr(pos); + + // Parse MAJOR.MINOR.MICRO + u32 major = 0, minor = 0, micro = 0; + int dots = 0; + std::string part; + for (char c : verStr) { + if (c >= '0' && c <= '9') { + part += c; + } else if (c == '.') { + if (part.empty()) { + return SYSENV_RC(SysEnvResult_InvalidVersionFormat); + } + if (dots == 0) { + major = std::stoul(part); + } else if (dots == 1) { + minor = std::stoul(part); + } + part.clear(); + dots++; + } else { + return SYSENV_RC(SysEnvResult_InvalidVersionFormat); + } + } + if (!part.empty()) { + if (dots == 0) { + major = std::stoul(part); + } else { + micro = std::stoul(part); + } + } + + if (dots > 2) { + return SYSENV_RC(SysEnvResult_InvalidVersionFormat); + } + + entry.hasVersionCheck = true; + entry.op = op; + entry.version = MAKEHOSVERSION(major, minor, micro); + + R_SUCCEED(); + } + + void ReadConfigEntries(std::ifstream &file, std::vector &entries) { std::string line; while (std::getline(file, line)) { if (!line.empty() && line.back() == '\r') { @@ -111,7 +250,34 @@ namespace fs { continue; } - entries.push_back(line); + ProgramEntry entry; + entry.hasVersionCheck = false; + entry.op = OP_EQ; + entry.version = 0; + + // Find '=' delimiter to split program ID and version condition + size_t eqPos = line.find('='); + if (eqPos != std::string::npos) { + entry.id = line.substr(0, eqPos); + // Trim trailing spaces from program ID + while (!entry.id.empty() && entry.id.back() == ' ') { + entry.id.pop_back(); + } + + std::string cond = line.substr(eqPos + 1); + Result rc = ParseVersionCondition(cond, entry); + if (R_FAILED(rc)) { + // Parse failed: skip this line and log + Log("Failed to parse version condition: %s", line.c_str()); + continue; + } + } else { + entry.id = line; + } + + if (!entry.id.empty()) { + entries.push_back(entry); + } } } @@ -141,7 +307,7 @@ namespace fs { return SYSENV_RC(SysEnvResult_EmptyConfig); } - Result ParseConfig(std::vector &entries, bool emuNand) { + Result ParseConfig(std::vector &entries, bool emuNand) { R_TRY(EnsureConfigExists()); std::ifstream file(PATH); diff --git a/sysmodule/source/fs.hpp b/sysmodule/source/fs.hpp index 5610fd6..08ad8e8 100644 --- a/sysmodule/source/fs.hpp +++ b/sysmodule/source/fs.hpp @@ -4,8 +4,23 @@ namespace fs { + enum VersionOp { + OP_EQ = 0, // = + OP_GE, // >= + OP_LE, // <= + OP_GT, // > + OP_LT, // < + }; + + struct ProgramEntry { + std::string id; + bool hasVersionCheck; // true if firmware version condition is set + u8 op; // VersionOp value + u32 version; // MAKEHOSVERSION(major, minor, micro) + }; + void Log(const char *log, ...); - void EditContent(std::vector &matchList, std::string &env, std::string &del); - Result ParseConfig(std::vector &entries, bool emuNand); + Result EditContent(std::vector &matchList, std::string &env, std::string &del, u32 currentVer); + Result ParseConfig(std::vector &entries, bool emuNand); } diff --git a/sysmodule/source/main.cpp b/sysmodule/source/main.cpp index 84a7729..bba6165 100644 --- a/sysmodule/source/main.cpp +++ b/sysmodule/source/main.cpp @@ -59,8 +59,9 @@ bool IsEmuNand() { } int main() { - std::vector entries; + std::vector entries; bool isEmunand = IsEmuNand(); + u32 currentVer = hosversionGet(); Result rc = fs::ParseConfig(entries, isEmunand); if (R_FAILED(rc)) { @@ -77,7 +78,7 @@ int main() { del = ".emu.bak"; } - fs::EditContent(entries, env, del); + fs::EditContent(entries, env, del, currentVer); return 0; } diff --git a/sysmodule/source/result.hpp b/sysmodule/source/result.hpp index 0feac01..59501b1 100644 --- a/sysmodule/source/result.hpp +++ b/sysmodule/source/result.hpp @@ -12,6 +12,7 @@ enum SysEnvResult { SysEnvResult_CreateFileFailed, SysEnvResult_OpenContentsFailed, SysEnvResult_RenameFailed, + SysEnvResult_InvalidVersionFormat, }; #define SYSENV_RC(x) MAKERESULT(SysEnvModule, x)