Skip to content

Commit 09b92a5

Browse files
committed
Changed: Generalize cloud folder detection beyond OneDrive
- Replace OneDrive-only substring/env-var checks with layered detection (OneDrive env vars, Cloud Files API via CfGetSyncRootInfoByPath, known cloud folder names) so Dropbox, Google Drive, iCloud, etc. are caught. - cldapi missing on older than Windows 10 1709 falls back gracefully. - Installer keeps two tiers (Desktop is only redirected by OneDrive).
1 parent d27bad7 commit 09b92a5

4 files changed

Lines changed: 192 additions & 26 deletions

File tree

source/Reloaded.Mod.Installer.Lib/Settings.cs

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,7 @@ public static Settings GetSettings(string[] args)
3535
private static string GetSafeInstallPath()
3636
{
3737
var installPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
38-
bool hasNonAsciiChars = installPath.Any(c => c > 127);
39-
if (IsPathInOneDrive(installPath) || hasNonAsciiChars)
38+
if (IsPathInCloudSyncFolder(installPath))
4039
{
4140
var driveRoot = Path.GetPathRoot(Environment.SystemDirectory);
4241
if (driveRoot == null)
@@ -48,15 +47,36 @@ private static string GetSafeInstallPath()
4847
}
4948

5049
/// <summary>
51-
/// Checks whether a given path is inside a OneDrive-managed folder.
52-
/// Uses the OneDrive environment variables.
50+
/// Checks whether a given path is inside a cloud sync folder (OneDrive, Dropbox, Google Drive,
51+
/// iCloud, Box, MEGA, etc.). Reloaded installed into such folders is avoided because many mods
52+
/// do not tolerate cloud offload/locking, and load times are poor.
53+
///
54+
/// Detection is layered, checked in order:
55+
///
56+
/// - OneDrive environment variables (<c>OneDrive</c> / <c>OneDriveCommercial</c>).
57+
///
58+
/// - The Windows Cloud Files API (<c>CfGetSyncRootInfoByPath</c>, available on Windows 10 1709+),
59+
/// which detects any provider that registers a sync root.
60+
///
61+
/// Desktop is only ever redirected by OneDrive, so these two tiers are sufficient for the install-path check.
5362
/// </summary>
54-
private static bool IsPathInOneDrive(string path)
63+
private static bool IsPathInCloudSyncFolder(string path)
5564
{
5665
if (string.IsNullOrEmpty(path))
5766
return false;
5867

59-
foreach (var envVar in new[] { "OneDrive", "OneDriveCommercial" })
68+
string fullPath;
69+
try { fullPath = Path.GetFullPath(path); }
70+
catch { return false; }
71+
72+
return IsInOneDrive(fullPath)
73+
|| IsInRegisteredCloudSyncRoot(fullPath);
74+
}
75+
76+
// --- Tier 1: OneDrive environment variables ---
77+
private static bool IsInOneDrive(string fullPath)
78+
{
79+
foreach (var envVar in s_oneDriveEnvVars)
6080
{
6181
var root = Environment.GetEnvironmentVariable(envVar);
6282
if (string.IsNullOrEmpty(root))
@@ -66,15 +86,58 @@ private static bool IsPathInOneDrive(string path)
6686
{
6787
var fullRoot = Path.GetFullPath(root)
6888
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
69-
var fullPath = Path.GetFullPath(path)
70-
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
71-
72-
if (fullPath.StartsWith(fullRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
89+
if (IsUnder(fullPath, fullRoot))
7390
return true;
7491
}
75-
catch { /* malformed path - skip */ }
92+
catch { /* malformed path/env - skip */ }
7693
}
7794

7895
return false;
7996
}
97+
98+
// --- Tier 2: Cloud Files API (cldapi.dll), Windows 10 1709+ ---
99+
// A single native call reports whether the path is inside any registered cloud sync root,
100+
// regardless of provider. No ancestor walk or hydration-state dependence.
101+
private static bool IsInRegisteredCloudSyncRoot(string fullPath)
102+
{
103+
try
104+
{
105+
// CF_SYNC_ROOT_INFO_CLASS.CfSyncRootInfoBasic == 0.
106+
// A small buffer is plenty for the basic info struct; we only care about the HRESULT.
107+
var buffer = new byte[256];
108+
int hr = CfGetSyncRootInfoByPath(fullPath, 0, buffer, buffer.Length, out _);
109+
return hr >= 0; // S_OK (0) => the path resolves to a registered sync root.
110+
}
111+
catch
112+
{
113+
// cldapi.dll missing (older than Windows 10 1709) or call failed: rely on other tiers.
114+
return false;
115+
}
116+
}
117+
118+
/// <summary>
119+
/// True if <paramref name="fullPath"/> equals or is a descendant of <paramref name="root"/>.
120+
/// Case-insensitive. The separator is appended on the prefix check so a root named
121+
/// e.g. "OneDrive" does not match a sibling like "OneDriveBackup".
122+
/// </summary>
123+
private static bool IsUnder(string fullPath, string root)
124+
{
125+
var trimmedRoot = root.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
126+
if (trimmedRoot.Length == 0)
127+
return false;
128+
129+
return fullPath.Equals(trimmedRoot, StringComparison.OrdinalIgnoreCase)
130+
|| fullPath.StartsWith(trimmedRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase);
131+
}
132+
133+
[DllImport("cldapi.dll", CharSet = CharSet.Unicode)]
134+
[return: MarshalAs(UnmanagedType.I4)]
135+
private static extern int CfGetSyncRootInfoByPath(
136+
string filePath,
137+
int infoClass,
138+
[Out] byte[] infoBuffer,
139+
int infoBufferSize,
140+
out int returnedLength);
141+
142+
private static readonly string[] s_oneDriveEnvVars = { "OneDrive", "OneDriveCommercial" };
80143
}

source/Reloaded.Mod.Launcher.Lib/Commands/Application/AddApplicationCommand.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@ static string GetProductName(string exePath)
6262
try { exePath = SymlinkResolver.GetFinalPathName(exePath); }
6363
catch (Exception e) { Errors.HandleException(e, Resources.ErrorAddApplicationCantReadSymlink.Get()); }
6464

65-
// Warn if OneDrive or NonAsciiChars detected in Game Path
65+
// Warn if the Game Path is inside a cloud sync folder (OneDrive, Dropbox, ...) or has NonAsciiChars
6666
bool hasNonAsciiChars = exePath.Any(c => c > 127);
67-
if (PathUtility.IsPathInOneDrive(exePath) || hasNonAsciiChars)
67+
if (PathUtility.IsPathInCloudSyncFolder(exePath) || hasNonAsciiChars)
6868
{
6969
var confirmAddAnyway = Actions.DisplayMessagebox.Invoke(Resources.ProblematicPathTitle.Get(), Resources.ProblematicPathAppDescription.Get(), new Actions.DisplayMessageBoxParams()
7070
{
Lines changed: 112 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
using System.Runtime.InteropServices;
2+
using Environment = System.Environment;
3+
14
namespace Reloaded.Mod.Launcher.Lib.Utility;
25

36
/// <summary>
@@ -6,37 +9,137 @@ namespace Reloaded.Mod.Launcher.Lib.Utility;
69
public static class PathUtility
710
{
811
/// <summary>
9-
/// Checks whether a given path is inside a OneDrive-managed folder.
10-
/// Uses the <c>OneDrive</c> / <c>OneDriveCommercial</c> environment variables.
12+
/// Checks whether a given path is inside a cloud sync folder (OneDrive, Dropbox, Google Drive,
13+
/// iCloud, Box, MEGA, etc.). Reloaded and games inside such folders are avoided because many mods
14+
/// do not tolerate cloud offload/locking, and load times are poor.
15+
///
16+
/// Detection is layered, checked in order:
17+
///
18+
/// - OneDrive environment variables (<c>OneDrive</c> / <c>OneDriveCommercial</c>).
19+
///
20+
/// - The Windows Cloud Files API (<c>CfGetSyncRootInfoByPath</c>, available on Windows 10 1709+),
21+
/// which detects any provider that registers a sync root (OneDrive, Dropbox, iCloud, Box, ...).
22+
///
23+
/// - Known cloud folder names under the user profile, as a fallback for older systems or
24+
/// providers that do not register a sync root.
1125
/// </summary>
1226
/// <param name="path">The path to check.</param>
13-
/// <returns>True if the path is inside a OneDrive root; false otherwise.</returns>
14-
public static bool IsPathInOneDrive(string path)
27+
/// <returns>True if the path is inside a cloud-synced folder; false otherwise.</returns>
28+
public static bool IsPathInCloudSyncFolder(string path)
1529
{
1630
if (string.IsNullOrEmpty(path))
1731
return false;
1832

33+
string fullPath;
34+
try { fullPath = Path.GetFullPath(path); }
35+
catch { return false; }
36+
37+
return IsInOneDrive(fullPath)
38+
|| IsInRegisteredCloudSyncRoot(fullPath)
39+
|| IsInKnownCloudFolder(fullPath);
40+
}
41+
42+
// --- Tier 1: OneDrive environment variables ---
43+
private static bool IsInOneDrive(string fullPath)
44+
{
1945
foreach (var envVar in s_oneDriveEnvVars)
2046
{
21-
var root = System.Environment.GetEnvironmentVariable(envVar);
47+
var root = Environment.GetEnvironmentVariable(envVar);
2248
if (string.IsNullOrEmpty(root))
2349
continue;
2450

2551
try
2652
{
2753
var fullRoot = Path.GetFullPath(root)
2854
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
29-
var fullPath = Path.GetFullPath(path)
30-
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
55+
if (IsUnder(fullPath, fullRoot))
56+
return true;
57+
}
58+
catch { /* malformed path/env - skip */ }
59+
}
3160

32-
if (fullPath.StartsWith(fullRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
61+
return false;
62+
}
63+
64+
// --- Tier 2: Cloud Files API (cldapi.dll), Windows 10 1709+ ---
65+
// A single native call reports whether the path is inside any registered cloud sync root,
66+
// regardless of provider. No ancestor walk or hydration-state dependence.
67+
private static bool IsInRegisteredCloudSyncRoot(string fullPath)
68+
{
69+
try
70+
{
71+
// CF_SYNC_ROOT_INFO_CLASS.CfSyncRootInfoBasic == 0.
72+
// A small buffer is plenty for the basic info struct; we only care about the HRESULT.
73+
var buffer = new byte[256];
74+
int hr = CfGetSyncRootInfoByPath(fullPath, 0, buffer, buffer.Length, out _);
75+
return hr >= 0; // S_OK (0) => the path resolves to a registered sync root.
76+
}
77+
catch
78+
{
79+
// cldapi.dll missing (older than Windows 10 1709) or call failed: rely on other tiers.
80+
return false;
81+
}
82+
}
83+
84+
// --- Tier 3: known cloud folder names under the user profile ---
85+
private static bool IsInKnownCloudFolder(string fullPath)
86+
{
87+
var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
88+
if (string.IsNullOrEmpty(userProfile))
89+
return false;
90+
91+
string fullProfile;
92+
try { fullProfile = Path.GetFullPath(userProfile).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); }
93+
catch { return false; }
94+
95+
foreach (var folder in s_knownCloudFolders)
96+
{
97+
try
98+
{
99+
if (IsUnder(fullPath, Path.Combine(fullProfile, folder)))
33100
return true;
34101
}
35-
catch { /* malformed path - skip */ }
102+
catch { /* malformed name - skip */ }
36103
}
37104

38105
return false;
39106
}
40107

108+
/// <summary>
109+
/// True if <paramref name="fullPath"/> equals or is a descendant of <paramref name="root"/>.
110+
/// Case-insensitive. The separator is appended on the prefix check so a root named
111+
/// e.g. "OneDrive" does not match a sibling like "OneDriveBackup".
112+
/// </summary>
113+
private static bool IsUnder(string fullPath, string root)
114+
{
115+
var trimmedRoot = root.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
116+
if (trimmedRoot.Length == 0)
117+
return false;
118+
119+
return fullPath.Equals(trimmedRoot, StringComparison.OrdinalIgnoreCase)
120+
|| fullPath.StartsWith(trimmedRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase);
121+
}
122+
123+
[DllImport("cldapi.dll", CharSet = CharSet.Unicode)]
124+
[return: MarshalAs(UnmanagedType.I4)]
125+
private static extern int CfGetSyncRootInfoByPath(
126+
string filePath,
127+
int infoClass,
128+
[Out] byte[] infoBuffer,
129+
int infoBufferSize,
130+
out int returnedLength);
131+
41132
private static readonly string[] s_oneDriveEnvVars = { "OneDrive", "OneDriveCommercial" };
133+
134+
private static readonly string[] s_knownCloudFolders =
135+
{
136+
"Dropbox",
137+
"Google Drive",
138+
"GoogleDrive",
139+
"iCloudDrive",
140+
"Box Sync",
141+
"Box",
142+
"MEGA",
143+
"pCloud"
144+
};
42145
}

source/Reloaded.Mod.Launcher/App.xaml.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ private void OnStartup(object sender, StartupEventArgs e)
4242
// Need to construct MainWindow before invoking any dialog, otherwise Shutdown will be called on closing the dialog
4343
var window = new MainWindow();
4444

45-
// Warn if OneDrive or NonAsciiChars detected in Reloaded-II directory
45+
// Warn if the Reloaded-II directory is inside a cloud sync folder (OneDrive, Dropbox, ...) or has NonAsciiChars
4646
bool reloadedPathHasNonAsciiChars = AppContext.BaseDirectory.Any(c => c > 127);
47-
if (PathUtility.IsPathInOneDrive(AppContext.BaseDirectory) || reloadedPathHasNonAsciiChars)
47+
if (PathUtility.IsPathInCloudSyncFolder(AppContext.BaseDirectory) || reloadedPathHasNonAsciiChars)
4848
{
4949
Actions.DisplayMessagebox.Invoke(Lib.Static.Resources.ProblematicPathTitle.Get(), Lib.Static.Resources.ProblematicPathReloadedDescription.Get(), new Actions.DisplayMessageBoxParams()
5050
{
@@ -54,12 +54,12 @@ private void OnStartup(object sender, StartupEventArgs e)
5454
}
5555
else // We only do this check if the Reloaded-II directory check passed
5656
{
57-
// Warn if OneDrive or NonAsciiChars detected in Mods directory
57+
// Warn if the Mods directory is inside a cloud sync folder (OneDrive, Dropbox, ...) or has NonAsciiChars
5858
var modsDirectory = Lib.IoC.Get<LoaderConfig>().GetModConfigDirectory();
5959
if (modsDirectory != null)
6060
{
6161
bool modsDirectoryPathHasNonAsciiChars = modsDirectory.Any(c => c > 127);
62-
if (PathUtility.IsPathInOneDrive(modsDirectory) || modsDirectoryPathHasNonAsciiChars)
62+
if (PathUtility.IsPathInCloudSyncFolder(modsDirectory) || modsDirectoryPathHasNonAsciiChars)
6363
{
6464
Actions.DisplayMessagebox.Invoke(Lib.Static.Resources.ProblematicPathTitle.Get(), Lib.Static.Resources.ProblematicPathModsDescription.Get(), new Actions.DisplayMessageBoxParams()
6565
{

0 commit comments

Comments
 (0)