Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ build

*.nsp
*.nso
*.npdm
*.npdm
*.zip
sdout/
.DS_Store
25 changes: 25 additions & 0 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,23 @@ You have two sections: <br>
Order doesn’t matter. <br>
Just add the program ID(s) under whichever environment you want them blocked in. <br>

### Firmware Version Conditions (Optional)

Each program ID can optionally include a firmware version condition. <br>
When a condition is set, the program is **only blocked if the current firmware does NOT match the condition**. <br>

Supported operators: `=` (equal), `>=`, `<=`, `>`, `<` <br>

Format: `<program_id> = <operator><major.minor.micro>` <br>

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``. <br>
Expand All @@ -41,5 +58,13 @@ For instance this config will block themes on ``SysNand``. <br>
[EmuNand]
```

With firmware version conditions — only block a theme if firmware is not 16.0.0: <br>
```
[SysNand]
0100000000001000 = 16.0.0

[EmuNand]
```

## Thanks to
- Arcdelta for motivating me to actually finish this lmao.
20 changes: 17 additions & 3 deletions sysmodule/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 ...
Expand All @@ -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
Expand Down
31 changes: 31 additions & 0 deletions sysmodule/config.ini.template
Original file line number Diff line number Diff line change
@@ -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>[ = <op><major.minor.micro>]
;
; 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
186 changes: 176 additions & 10 deletions sysmodule/source/fs.cpp
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
#include <switch.h>
#include <fstream>
#include <string>
#include <iostream>
#include <vector>
#include <dirent.h>
#include <algorithm>
#include <sys/stat.h>
#include <cstdarg>
#include "result.hpp"
#include "fs.hpp"

namespace fs {

Expand Down Expand Up @@ -36,7 +35,22 @@ namespace fs {
return str.compare(str.length() - suffix.length(), suffix.length(), suffix) == 0;
}

Result EditContent(std::vector<std::string> &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<ProgramEntry> &matchList, std::string &env, std::string &del, u32 currentVer) {
DIR *dir = opendir(CONTENTS);
if (!dir) {
return SYSENV_RC(SysEnvResult_OpenContentsFailed);
Expand All @@ -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());
}
}

Expand Down Expand Up @@ -96,7 +144,98 @@ namespace fs {
return SYSENV_RC(SysEnvResult_HeaderMissing);
}

void ReadConfigEntries(std::ifstream &file, std::vector<std::string> &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<ProgramEntry> &entries) {
std::string line;
while (std::getline(file, line)) {
if (!line.empty() && line.back() == '\r') {
Expand All @@ -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);
}
}
}

Expand Down Expand Up @@ -141,7 +307,7 @@ namespace fs {
return SYSENV_RC(SysEnvResult_EmptyConfig);
}

Result ParseConfig(std::vector<std::string> &entries, bool emuNand) {
Result ParseConfig(std::vector<ProgramEntry> &entries, bool emuNand) {
R_TRY(EnsureConfigExists());

std::ifstream file(PATH);
Expand Down
19 changes: 17 additions & 2 deletions sysmodule/source/fs.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string> &matchList, std::string &env, std::string &del);
Result ParseConfig(std::vector<std::string> &entries, bool emuNand);
Result EditContent(std::vector<ProgramEntry> &matchList, std::string &env, std::string &del, u32 currentVer);
Result ParseConfig(std::vector<ProgramEntry> &entries, bool emuNand);

}
Loading