@@ -13,26 +13,66 @@ static bool isDir(const std::string& path) {
1313 return stat (path.c_str (), &st) == 0 && S_ISDIR (st.st_mode );
1414}
1515
16+ // Iterate "/"-delimited boundaries instead of running mkdir at every char index.
17+ // Saves O(path-length) syscalls on slow SD-card filesystems.
1618static void mkdirp (const std::string& path) {
17- for (size_t i = 1 ; i <= path.size (); i++) {
18- if (i == path.size () || path[i] == ' /' )
19- mkdir (path.substr (0 , i).c_str (), 0755 );
19+ size_t pos = 1 ;
20+ while (pos < path.size ()) {
21+ size_t slash = path.find (' /' , pos);
22+ if (slash == std::string::npos) break ;
23+ mkdir (path.substr (0 , slash).c_str (), 0755 );
24+ pos = slash + 1 ;
2025 }
26+ mkdir (path.c_str (), 0755 );
2127}
2228
23- static bool rmrf (const std::string& path) {
29+ // Recursive delete with symlink-loop protection.
30+ // - Uses lstat (not stat) so symlinks are detected before recursion.
31+ // - Tracks visited (dev,inode) pairs so a directory hardlinked or
32+ // symlinked-into-itself can't trap us in infinite recursion.
33+ // - Caps recursion depth at 64 -- Wii U mods don't go that deep, anything
34+ // beyond is malicious or corrupt.
35+ #include < set>
36+ struct VisitedKey { dev_t dev; ino_t ino; };
37+ static bool operator <(const VisitedKey& a, const VisitedKey& b) {
38+ if (a.dev != b.dev ) return a.dev < b.dev ;
39+ return a.ino < b.ino ;
40+ }
41+
42+ static bool rmrfImpl (const std::string& path, std::set<VisitedKey>& visited, int depth) {
43+ if (depth > 64 ) {
44+ LOG_WARN (" rmrf: depth limit hit at %s -- aborting" , path.c_str ());
45+ return false ;
46+ }
47+ struct stat st;
48+ if (lstat (path.c_str (), &st) != 0 ) return true ; // already gone
49+ if (!S_ISDIR (st.st_mode )) {
50+ // Symlink or regular file -- remove without following.
51+ return remove (path.c_str ()) == 0 ;
52+ }
53+ VisitedKey key{st.st_dev , st.st_ino };
54+ if (!visited.insert (key).second ) {
55+ LOG_WARN (" rmrf: cycle detected at %s -- aborting" , path.c_str ());
56+ return false ;
57+ }
58+
2459 DIR * d = opendir (path.c_str ());
25- if (!d) { remove (path.c_str ()); return true ; }
60+ if (!d) return false ;
61+ bool ok = true ;
2662 struct dirent * e;
2763 while ((e = readdir (d)) != nullptr ) {
2864 std::string name = e->d_name ;
2965 if (name == " ." || name == " .." ) continue ;
30- std::string child = path + " /" + name;
31- if (isDir (child)) rmrf (child);
32- else ::remove (child.c_str ());
66+ if (!rmrfImpl (path + " /" + name, visited, depth + 1 )) ok = false ;
3367 }
3468 closedir (d);
35- return rmdir (path.c_str ()) == 0 ;
69+ if (rmdir (path.c_str ()) != 0 ) ok = false ;
70+ return ok;
71+ }
72+
73+ static bool rmrf (const std::string& path) {
74+ std::set<VisitedKey> visited;
75+ return rmrfImpl (path, visited, 0 );
3676}
3777
3878static std::vector<std::string> listDirs (const std::string& path) {
@@ -115,6 +155,17 @@ bool InstalledScanner::setActive(InstalledMod& mod, bool active) {
115155
116156 mkdirp (dstDir);
117157
158+ // Some platforms refuse rename() if dst exists. If a stale folder is in
159+ // the way (e.g. previous failed activation), remove it first.
160+ struct stat st;
161+ if (stat (dst.c_str (), &st) == 0 ) {
162+ LOG_WARN (" InstalledScanner: rename target exists, removing first: %s" , dst.c_str ());
163+ if (!rmrf (dst)) {
164+ LOG_ERROR (" InstalledScanner: could not clear rename target: %s" , dst.c_str ());
165+ return false ;
166+ }
167+ }
168+
118169 if (rename (src.c_str (), dst.c_str ()) != 0 ) {
119170 LOG_ERROR (" InstalledScanner: rename failed: %s -> %s" , src.c_str (), dst.c_str ());
120171 return false ;
0 commit comments