-
-
Notifications
You must be signed in to change notification settings - Fork 404
Adds function to case insensitively resolve cloud save path #701
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,12 @@ import okhttp3.RequestBody.Companion.toRequestBody | |
| import org.json.JSONArray | ||
| import org.json.JSONObject | ||
| import timber.log.Timber | ||
| import kotlin.io.path.Path | ||
| import kotlin.io.path.absolutePathString | ||
| import kotlin.io.path.exists | ||
| import kotlin.io.path.isDirectory | ||
| import kotlin.io.path.listDirectoryEntries | ||
| import kotlin.io.path.name | ||
|
|
||
| /** | ||
| * Manages Epic Cloud Saves - downloading and uploading save files | ||
|
|
@@ -1193,20 +1199,8 @@ object EpicCloudSavesManager { | |
| "{appname}" to game.appName, | ||
| ) | ||
|
|
||
| // Map to Wine prefix paths (like GOG does) | ||
| // Check for both proper casing (AppData) and legacy lowercase (appdata) | ||
| val usersPath = File(winePrefix, "drive_c/users/$user") | ||
| val appDataDir = when { | ||
| File(usersPath, "AppData").exists() -> "AppData" | ||
| File(usersPath, "appdata").exists() -> "appdata" | ||
| File(usersPath, "appData").exists() -> "appData" | ||
| else -> "AppData" // Default to proper Windows casing | ||
| } | ||
|
|
||
| Timber.tag("Epic").d("[Cloud Saves] Using AppData directory name: $appDataDir") | ||
|
|
||
| val appDataPath = File(winePrefix, "drive_c/users/$user/$appDataDir/Local").absolutePath | ||
| val appDataRoamingPath = File(winePrefix, "drive_c/users/$user/$appDataDir/Roaming").absolutePath | ||
| val appDataPath = File(winePrefix, "drive_c/users/$user/AppData/Local").absolutePath | ||
| val appDataRoamingPath = File(winePrefix, "drive_c/users/$user/AppData/Roaming").absolutePath | ||
| val documentsPath = File(winePrefix, "drive_c/users/$user/Documents").absolutePath | ||
| val savedGamesPath = File(winePrefix, "drive_c/users/$user/Saved Games").absolutePath | ||
|
|
||
|
|
@@ -1252,7 +1246,8 @@ object EpicCloudSavesManager { | |
| } | ||
| } | ||
|
|
||
| val finalPath = File(normalizedParts.joinToString("/")) | ||
| val resolvedWinePath: String = resolveCaseInsensitivity(normalizedParts.joinToString("/")) | ||
| val finalPath = File(resolvedWinePath) | ||
|
Comment on lines
+1249
to
+1250
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Windows drive-letter paths currently resolve incorrectly ( At Line 1299, resolution always starts from 💡 Proposed fix- val resolvedWinePath: String = resolveCaseInsensitivity(normalizedParts.joinToString("/"))
+ val resolvedWinePath: String = resolveCaseInsensitivity(winePrefix, normalizedParts.joinToString("/"))
val finalPath = File(resolvedWinePath)- private fun resolveCaseInsensitivity(path: String): String {
- val pathParts = path.replace("\\", "/").split("/")
- var currentPath = Path("/")
+ private fun resolveCaseInsensitivity(winePrefix: String, path: String): String {
+ val normalized = path.replace("\\", "/")
+ val driveLetter = Regex("^[A-Za-z]:").find(normalized)?.value?.firstOrNull()?.lowercaseChar()
+ val strippedPath = if (driveLetter != null) normalized.drop(2).trimStart('/') else normalized
+ val pathParts = strippedPath.split("/").filter { it.isNotEmpty() }
+
+ var currentPath = when {
+ driveLetter != null -> Path(winePrefix, "drive_$driveLetter")
+ normalized.startsWith("/") -> Path("/")
+ else -> Path(winePrefix)
+ }
for (segment in pathParts) {
// If we're at a place where the current path doesn't exist,
// just resolve as-is and continue
if (!currentPath.exists() || !currentPath.isDirectory()) {
currentPath = currentPath.resolve(segment)
continue
}
// Avoid directory traversal if possible by trying to directly resolve
val exactMatchPath = currentPath.resolve(segment)
if (exactMatchPath.exists()) {
currentPath = exactMatchPath
continue
}
val match = try {
currentPath.listDirectoryEntries().firstOrNull {
it.name.equals(segment, ignoreCase = true)
}
} catch (_: Exception) {
null
}
// Use matched casing if not, otherwise fallback to requested casing
currentPath = match ?: currentPath.resolve(segment)
}
return currentPath.absolutePathString()
}Also applies to: 1298-1331 🤖 Prompt for AI Agents |
||
|
|
||
| // Check subdirectories for save files | ||
| // Some games store saves in user-specific subdirectories (e.g., "0/", "1/", etc.) | ||
|
|
@@ -1278,7 +1273,7 @@ object EpicCloudSavesManager { | |
| } | ||
|
|
||
| // Always check for subdirectories with files | ||
| val subDirs = finalPath.listFiles { it -> it.isDirectory } ?: emptyArray() | ||
| val subDirs = finalPath.listFiles { it.isDirectory } ?: emptyArray() | ||
| val dirWithFiles = subDirs.firstOrNull { subDir -> | ||
| subDir.listFiles()?.any { it.isFile } == true | ||
| } | ||
|
|
@@ -1300,6 +1295,41 @@ object EpicCloudSavesManager { | |
| return actualPath | ||
| } | ||
|
|
||
| // Resolve a path case insensitively if there's no direct casing match | ||
| private fun resolveCaseInsensitivity(path: String): String { | ||
| val pathParts = path.replace("\\", "/").split("/") | ||
| var currentPath = Path("/") | ||
|
|
||
| for (segment in pathParts) { | ||
| // If we're at a place where the current path doesn't exist, | ||
| // just resolve as-is and continue | ||
| if (!currentPath.exists() || !currentPath.isDirectory()) { | ||
| currentPath = currentPath.resolve(segment) | ||
| continue | ||
| } | ||
|
|
||
| // Avoid directory traversal if possible by trying to directly resolve | ||
| val exactMatchPath = currentPath.resolve(segment) | ||
| if (exactMatchPath.exists()) { | ||
| currentPath = exactMatchPath | ||
| continue | ||
| } | ||
|
|
||
| val match = try { | ||
| currentPath.listDirectoryEntries().firstOrNull { | ||
| it.name.equals(segment, ignoreCase = true) | ||
| } | ||
| } catch (_: Exception) { | ||
| null | ||
| } | ||
|
|
||
| // Use matched casing if not, otherwise fallback to requested casing | ||
| currentPath = match ?: currentPath.resolve(segment) | ||
| } | ||
|
|
||
| return currentPath.absolutePathString() | ||
| } | ||
|
Comment on lines
+1299
to
+1331
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you explain this method please?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah the idea here is that we have a full unix-style path (ie "/data/app/0/app.gamenative/winePrefix/drive_c/users/xuser/AppData/locallow/Dodge Roll/Enter The Gungeon/LocalA.save"). After splitting up into the different pieces (ie ["data", "app", "0", "app.gamenative"...], we start from the root and see if "data" matches exactly case sensitively. This is to avoid directory traversals for every level if the path already exists. This will keep going until we get to a path segment we can't resolve case sensitively, locallow in this example. That point, it will list the directories of AppData and then check to see if there's any case insensitive matches, which should find "LocalLow" and resolve that instead. After that, the "Dodge Roll" and subsequent directories cannot be found at all so we'll fall into 1327 where "Dodge Roll" is added with its original casing. For "Enter the Gungeon", currentPath doesn't exist since "Dodge Roll" so anything further will fall into the 1307 branch and add the remainder with the original casing. So in this scenario, we'd expect so we can avoid creating that second directory. |
||
|
|
||
| private fun getSyncTimestamp(context: Context, appId: Int): String? { | ||
| val prefs = context.getSharedPreferences("epic_cloud_saves", Context.MODE_PRIVATE) | ||
| return prefs.getString("sync_timestamp_$appId", null) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: Windows-rooted save paths (
C:/...) are no longer translated to Winedrive_c, causing incorrect absolute resolution outside the Wine prefix.Prompt for AI agents