Skip to content
Merged
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
98 changes: 96 additions & 2 deletions source/Reloaded.Mod.Installer.Lib/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,7 @@ public static Settings GetSettings(string[] args)
private static string GetSafeInstallPath()
{
var installPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
bool hasNonAsciiChars = installPath.Any(c => c > 127);
if (installPath.Contains("OneDrive") || hasNonAsciiChars)
if (IsPathInCloudSyncFolder(installPath))
{
var driveRoot = Path.GetPathRoot(Environment.SystemDirectory);
if (driveRoot == null)
Expand All @@ -46,4 +45,99 @@ private static string GetSafeInstallPath()
}
return installPath;
}

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

string fullPath;
try { fullPath = Path.GetFullPath(path); }
catch { return false; }

return IsInOneDrive(fullPath)
|| IsInRegisteredCloudSyncRoot(fullPath);
}
Comment thread
Sewer56 marked this conversation as resolved.

// --- Tier 1: OneDrive environment variables ---
private static bool IsInOneDrive(string fullPath)
{
foreach (var envVar in s_oneDriveEnvVars)
{
var root = Environment.GetEnvironmentVariable(envVar);
if (string.IsNullOrEmpty(root))
continue;

try
{
var fullRoot = Path.GetFullPath(root)
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
if (IsUnder(fullPath, fullRoot))
return true;
}
catch { /* malformed path/env - skip */ }
}

return false;
}

// --- Tier 2: Cloud Files API (cldapi.dll), Windows 10 1709+ ---
// A single native call reports whether the path is inside any registered cloud sync root,
// regardless of provider. No ancestor walk or hydration-state dependence.
private static bool IsInRegisteredCloudSyncRoot(string fullPath)
{
try
{
// CF_SYNC_ROOT_INFO_CLASS.CfSyncRootInfoBasic == 0.
// A small buffer is plenty for the basic info struct; we only care about the HRESULT.
var buffer = new byte[256];
int hr = CfGetSyncRootInfoByPath(fullPath, 0, buffer, buffer.Length, out _);
return hr >= 0; // S_OK (0) => the path resolves to a registered sync root.
}
catch
{
// cldapi.dll missing (older than Windows 10 1709) or call failed: rely on other tiers.
return false;
}
}

/// <summary>
/// True if <paramref name="fullPath"/> equals or is a descendant of <paramref name="root"/>.
/// Case-insensitive. The separator is appended on the prefix check so a root named
/// e.g. "OneDrive" does not match a sibling like "OneDriveBackup".
/// </summary>
private static bool IsUnder(string fullPath, string root)
{
var trimmedRoot = root.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
if (trimmedRoot.Length == 0)
return false;

return fullPath.Equals(trimmedRoot, StringComparison.OrdinalIgnoreCase)
|| fullPath.StartsWith(trimmedRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase);
}

[DllImport("cldapi.dll", CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.I4)]
private static extern int CfGetSyncRootInfoByPath(
string filePath,
int infoClass,
[Out] byte[] infoBuffer,
int infoBufferSize,
out int returnedLength);

private static readonly string[] s_oneDriveEnvVars = { "OneDrive", "OneDriveCommercial" };
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ static string GetProductName(string exePath)
try { exePath = SymlinkResolver.GetFinalPathName(exePath); }
catch (Exception e) { Errors.HandleException(e, Resources.ErrorAddApplicationCantReadSymlink.Get()); }

// Warn if OneDrive or NonAsciiChars detected in Game Path
// Warn if the Game Path is inside a cloud sync folder (OneDrive, Dropbox, ...) or has NonAsciiChars
bool hasNonAsciiChars = exePath.Any(c => c > 127);
if (exePath.Contains("OneDrive") || hasNonAsciiChars)
if (PathUtility.IsPathInCloudSyncFolder(exePath) || hasNonAsciiChars)
{
var confirmAddAnyway = Actions.DisplayMessagebox.Invoke(Resources.ProblematicPathTitle.Get(), Resources.ProblematicPathAppDescription.Get(), new Actions.DisplayMessageBoxParams()
{
Expand Down
145 changes: 145 additions & 0 deletions source/Reloaded.Mod.Launcher.Lib/Utility/PathUtility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
using System.Runtime.InteropServices;
using Environment = System.Environment;

namespace Reloaded.Mod.Launcher.Lib.Utility;

/// <summary>
/// Utility methods for inspecting file system paths.
/// </summary>
public static class PathUtility
{
/// <summary>
/// Checks whether a given path is inside a cloud sync folder (OneDrive, Dropbox, Google Drive,
/// iCloud, Box, MEGA, etc.). Reloaded and games inside such folders are avoided because many mods
/// do not tolerate cloud offload/locking, and load times are poor.
///
/// Detection is layered, checked in order:
///
/// - OneDrive environment variables (<c>OneDrive</c> / <c>OneDriveCommercial</c>).
///
/// - The Windows Cloud Files API (<c>CfGetSyncRootInfoByPath</c>, available on Windows 10 1709+),
/// which detects any provider that registers a sync root (OneDrive, Dropbox, iCloud, Box, ...).
///
/// - Known cloud folder names under the user profile, as a fallback for older systems or
/// providers that do not register a sync root.
/// </summary>
/// <param name="path">The path to check.</param>
/// <returns>True if the path is inside a cloud-synced folder; false otherwise.</returns>
public static bool IsPathInCloudSyncFolder(string path)
{
if (string.IsNullOrEmpty(path))
return false;

string fullPath;
try { fullPath = Path.GetFullPath(path); }
catch { return false; }

return IsInOneDrive(fullPath)
|| IsInRegisteredCloudSyncRoot(fullPath)
|| IsInKnownCloudFolder(fullPath);
}

// --- Tier 1: OneDrive environment variables ---
private static bool IsInOneDrive(string fullPath)
{
foreach (var envVar in s_oneDriveEnvVars)
{
var root = Environment.GetEnvironmentVariable(envVar);
if (string.IsNullOrEmpty(root))
continue;

try
{
var fullRoot = Path.GetFullPath(root)
.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
if (IsUnder(fullPath, fullRoot))
return true;
}
catch { /* malformed path/env - skip */ }
}

return false;
}

// --- Tier 2: Cloud Files API (cldapi.dll), Windows 10 1709+ ---
// A single native call reports whether the path is inside any registered cloud sync root,
// regardless of provider. No ancestor walk or hydration-state dependence.
private static bool IsInRegisteredCloudSyncRoot(string fullPath)
{
try
{
// CF_SYNC_ROOT_INFO_CLASS.CfSyncRootInfoBasic == 0.
// A small buffer is plenty for the basic info struct; we only care about the HRESULT.
var buffer = new byte[256];
int hr = CfGetSyncRootInfoByPath(fullPath, 0, buffer, buffer.Length, out _);
return hr >= 0; // S_OK (0) => the path resolves to a registered sync root.
}
catch
{
// cldapi.dll missing (older than Windows 10 1709) or call failed: rely on other tiers.
return false;
}
}

// --- Tier 3: known cloud folder names under the user profile ---
private static bool IsInKnownCloudFolder(string fullPath)
{
var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
if (string.IsNullOrEmpty(userProfile))
return false;

string fullProfile;
try { fullProfile = Path.GetFullPath(userProfile).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); }
catch { return false; }

foreach (var folder in s_knownCloudFolders)
{
try
{
if (IsUnder(fullPath, Path.Combine(fullProfile, folder)))
return true;
}
catch { /* malformed name - skip */ }
}

return false;
}

/// <summary>
/// True if <paramref name="fullPath"/> equals or is a descendant of <paramref name="root"/>.
/// Case-insensitive. The separator is appended on the prefix check so a root named
/// e.g. "OneDrive" does not match a sibling like "OneDriveBackup".
/// </summary>
private static bool IsUnder(string fullPath, string root)
{
var trimmedRoot = root.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
if (trimmedRoot.Length == 0)
return false;

return fullPath.Equals(trimmedRoot, StringComparison.OrdinalIgnoreCase)
|| fullPath.StartsWith(trimmedRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase);
}

[DllImport("cldapi.dll", CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.I4)]
private static extern int CfGetSyncRootInfoByPath(
string filePath,
int infoClass,
[Out] byte[] infoBuffer,
int infoBufferSize,
out int returnedLength);

private static readonly string[] s_oneDriveEnvVars = { "OneDrive", "OneDriveCommercial" };

private static readonly string[] s_knownCloudFolders =
{
"Dropbox",
"Google Drive",
"GoogleDrive",
"iCloudDrive",
"Box Sync",
"Box",
"MEGA",
"pCloud"
};
}
8 changes: 4 additions & 4 deletions source/Reloaded.Mod.Launcher/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ private void OnStartup(object sender, StartupEventArgs e)
// Need to construct MainWindow before invoking any dialog, otherwise Shutdown will be called on closing the dialog
var window = new MainWindow();

// Warn if OneDrive or NonAsciiChars detected in Reloaded-II directory
// Warn if the Reloaded-II directory is inside a cloud sync folder (OneDrive, Dropbox, ...) or has NonAsciiChars
bool reloadedPathHasNonAsciiChars = AppContext.BaseDirectory.Any(c => c > 127);
if (AppContext.BaseDirectory.Contains("OneDrive") || reloadedPathHasNonAsciiChars)
if (PathUtility.IsPathInCloudSyncFolder(AppContext.BaseDirectory) || reloadedPathHasNonAsciiChars)
{
Actions.DisplayMessagebox.Invoke(Lib.Static.Resources.ProblematicPathTitle.Get(), Lib.Static.Resources.ProblematicPathReloadedDescription.Get(), new Actions.DisplayMessageBoxParams()
{
Expand All @@ -54,12 +54,12 @@ private void OnStartup(object sender, StartupEventArgs e)
}
else // We only do this check if the Reloaded-II directory check passed
{
// Warn if OneDrive or NonAsciiChars detected in Mods directory
// Warn if the Mods directory is inside a cloud sync folder (OneDrive, Dropbox, ...) or has NonAsciiChars
var modsDirectory = Lib.IoC.Get<LoaderConfig>().GetModConfigDirectory();
if (modsDirectory != null)
{
bool modsDirectoryPathHasNonAsciiChars = modsDirectory.Any(c => c > 127);
if (modsDirectory.Contains("OneDrive") || modsDirectoryPathHasNonAsciiChars)
if (PathUtility.IsPathInCloudSyncFolder(modsDirectory) || modsDirectoryPathHasNonAsciiChars)
{
Actions.DisplayMessagebox.Invoke(Lib.Static.Resources.ProblematicPathTitle.Get(), Lib.Static.Resources.ProblematicPathModsDescription.Get(), new Actions.DisplayMessageBoxParams()
{
Expand Down
Loading
Loading