Skip to content

Commit 99bf76d

Browse files
authored
Merge pull request #883 from Reloaded-Project/improve-onedrive-detection-and-fix-file-exist-checks
Fixed: Directory scanner skipping OneDrive cloud folders
2 parents 4fd2012 + ba760d1 commit 99bf76d

5 files changed

Lines changed: 325 additions & 14 deletions

File tree

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

Lines changed: 96 additions & 2 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 (installPath.Contains("OneDrive") || hasNonAsciiChars)
38+
if (IsPathInCloudSyncFolder(installPath))
4039
{
4140
var driveRoot = Path.GetPathRoot(Environment.SystemDirectory);
4241
if (driveRoot == null)
@@ -46,4 +45,99 @@ private static string GetSafeInstallPath()
4645
}
4746
return installPath;
4847
}
48+
49+
/// <summary>
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.
62+
/// </summary>
63+
private static bool IsPathInCloudSyncFolder(string path)
64+
{
65+
if (string.IsNullOrEmpty(path))
66+
return false;
67+
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)
80+
{
81+
var root = Environment.GetEnvironmentVariable(envVar);
82+
if (string.IsNullOrEmpty(root))
83+
continue;
84+
85+
try
86+
{
87+
var fullRoot = Path.GetFullPath(root)
88+
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
89+
if (IsUnder(fullPath, fullRoot))
90+
return true;
91+
}
92+
catch { /* malformed path/env - skip */ }
93+
}
94+
95+
return false;
96+
}
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" };
49143
}

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 (exePath.Contains("OneDrive") || 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: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
using System.Runtime.InteropServices;
2+
using Environment = System.Environment;
3+
4+
namespace Reloaded.Mod.Launcher.Lib.Utility;
5+
6+
/// <summary>
7+
/// Utility methods for inspecting file system paths.
8+
/// </summary>
9+
public static class PathUtility
10+
{
11+
/// <summary>
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.
25+
/// </summary>
26+
/// <param name="path">The path to check.</param>
27+
/// <returns>True if the path is inside a cloud-synced folder; false otherwise.</returns>
28+
public static bool IsPathInCloudSyncFolder(string path)
29+
{
30+
if (string.IsNullOrEmpty(path))
31+
return false;
32+
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+
{
45+
foreach (var envVar in s_oneDriveEnvVars)
46+
{
47+
var root = Environment.GetEnvironmentVariable(envVar);
48+
if (string.IsNullOrEmpty(root))
49+
continue;
50+
51+
try
52+
{
53+
var fullRoot = Path.GetFullPath(root)
54+
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
55+
if (IsUnder(fullPath, fullRoot))
56+
return true;
57+
}
58+
catch { /* malformed path/env - skip */ }
59+
}
60+
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)))
100+
return true;
101+
}
102+
catch { /* malformed name - skip */ }
103+
}
104+
105+
return false;
106+
}
107+
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+
132+
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+
};
145+
}

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 (AppContext.BaseDirectory.Contains("OneDrive") || 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 (modsDirectory.Contains("OneDrive") || 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)