Skip to content

Commit 02ae68e

Browse files
committed
fix: complete imageFs.wineprefix migration in SteamUtils.kt
The cascade-fix commit (15716e2) said "every per-container write was switched to container.getRootDir() + /.wine" but 13 sites in SteamUtils.kt still resolved through imageFs.wineprefix or imageFs.rootDir+ImageFs.WINEPREFIX, which both go through the global xuser symlink. ensureSteamCfg in particular was called immediately after extractSteamFiles in the same launch flow that just took pains to use the per-container path — the cfg write then pivoted back through the symlink and was still cascade-vulnerable. Migration strategy: * appId-based functions (backupSteamclientFiles, restoreSteamclientFiles, restoreUnpackedExecutable, createAppManifest, restoreOriginalExecutable, migrateGSESavesToSteamUserdata, migrateSteamUserdataToGSESaves) now load the Container internally via ContainerUtils.getContainer(context, "STEAM_$steamAppId") and write through container.rootDir. No signature change, no caller updates needed. * imageFs-only functions (setupLightweightSteamConfig, ensureSteamCfg, logSteamBinaryFingerprint, purgePhantomAppUserdata, cleanupExtractedSteamFiles) gained an optional Container? parameter. When passed (launch-flow callers do), writes go to the per-container path. imageFs.wineprefix is retained as a fallback for callers without a container in scope (notably the SteamTokenLogin flow), preserving pre-fix behavior for that path. Callers in XServerScreen.kt, MainViewModel.kt, replaceSteamApi, restoreSteamApi all pass container. * updateOrModifyLocalConfig had container in scope already; just switched to use it. After this, every active per-container write in SteamUtils.kt goes through container.rootDir. The remaining imageFs.wineprefix references are either comments or symlink fallbacks gated behind container == null.
1 parent 57f05f2 commit 02ae68e

3 files changed

Lines changed: 74 additions & 55 deletions

File tree

app/src/main/java/app/gamenative/ui/model/MainViewModel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,7 @@ class MainViewModel @Inject constructor(
463463
container.putExtra("lastSteamMode", currentMode)
464464
container.saveData()
465465
if (previousMode == "real" && currentMode == "emu") {
466-
SteamUtils.cleanupExtractedSteamFiles(context)
466+
SteamUtils.cleanupExtractedSteamFiles(context, container)
467467
}
468468
}
469469
if (container.isLaunchRealSteam()) {

app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4138,9 +4138,9 @@ private fun setupWineSystemFiles(
41384138
SteamUtils.deleteTreeNoFollowSymlinks(steamDir)
41394139
}
41404140
extractSteamFiles(context, container, onExtractFileListener)
4141-
SteamUtils.ensureSteamCfg(ImageFs.find(context))
4142-
SteamUtils.purgePhantomAppUserdata(ImageFs.find(context), "241100")
4143-
SteamUtils.logSteamBinaryFingerprint(ImageFs.find(context), "prepareContainer:realSteam")
4141+
SteamUtils.ensureSteamCfg(ImageFs.find(context), container)
4142+
SteamUtils.purgePhantomAppUserdata(ImageFs.find(context), "241100", container)
4143+
SteamUtils.logSteamBinaryFingerprint(ImageFs.find(context), "prepareContainer:realSteam", container)
41444144
container.putExtra("steamExtractedForWine", steamExtractedKey)
41454145
containerDataChanged = true
41464146
}

app/src/main/java/app/gamenative/utils/SteamUtils.kt

Lines changed: 70 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ object SteamUtils {
306306
// another concurrent launch and corrupt that game's prefix).
307307
val container = ContainerUtils.getContainer(context, appId)
308308
autoLoginUserChanges(imageFs, container)
309-
setupLightweightSteamConfig(imageFs, SteamService.userSteamId?.toString())
309+
setupLightweightSteamConfig(imageFs, SteamService.userSteamId?.toString(), container)
310310

311311
val rootPath = Paths.get(appDirPath)
312312
// Get ticket once for all DLLs
@@ -435,15 +435,18 @@ object SteamUtils {
435435
}
436436

437437
fun backupSteamclientFiles(context: Context, steamAppId: Int) {
438-
val imageFs = ImageFs.find(context)
438+
// Per-container path; imageFs.wineprefix resolves through the global xuser
439+
// symlink which is unsafe for writes (see WineUtils.applySystemTweaks).
440+
val container = ContainerUtils.getContainer(context, "STEAM_$steamAppId")
441+
val steamDir = File(container.rootDir, ".wine/drive_c/Program Files (x86)/Steam")
439442

440443
var backupCount = 0
441444

442-
val backupDir = File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam/steamclient_backup")
445+
val backupDir = File(steamDir, "steamclient_backup")
443446
backupDir.mkdirs()
444447

445448
steamClientFiles().forEach { file ->
446-
val dll = File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam/$file")
449+
val dll = File(steamDir, file)
447450
if (dll.exists()) {
448451
Files.copy(dll.toPath(), File(backupDir, "$file.orig").toPath(), StandardCopyOption.REPLACE_EXISTING)
449452
backupCount++
@@ -479,13 +482,14 @@ object SteamUtils {
479482
}
480483

481484
fun restoreSteamclientFiles(context: Context, steamAppId: Int) {
482-
val imageFs = ImageFs.find(context)
485+
// Per-container path; imageFs.wineprefix resolves through the global xuser
486+
// symlink which is unsafe for writes (see WineUtils.applySystemTweaks).
487+
val container = ContainerUtils.getContainer(context, "STEAM_$steamAppId")
488+
val origDir = File(container.rootDir, ".wine/drive_c/Program Files (x86)/Steam")
483489

484490
var restoredCount = 0
485491

486-
val origDir = File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam")
487-
488-
val backupDir = File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam/steamclient_backup")
492+
val backupDir = File(origDir, "steamclient_backup")
489493
if (backupDir.exists()) {
490494
steamClientFiles().forEach { file ->
491495
val dll = File(backupDir, "$file.orig")
@@ -496,7 +500,7 @@ object SteamUtils {
496500
}
497501
}
498502

499-
val extraDllDir = File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam/extra_dlls")
503+
val extraDllDir = File(origDir, "extra_dlls")
500504
if (extraDllDir.exists()) {
501505
extraDllDir.deleteRecursively()
502506
}
@@ -599,10 +603,13 @@ object SteamUtils {
599603
* Creates configuration files that make Steam run in lightweight mode
600604
* with reduced resource usage and disabled community features
601605
*/
602-
private fun setupLightweightSteamConfig(imageFs: ImageFs, steamId64: String?) {
606+
private fun setupLightweightSteamConfig(imageFs: ImageFs, steamId64: String?, container: Container? = null) {
603607
Timber.i("Setting up lightweight steam configs")
604608
try {
605-
val steamPath = File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam")
609+
// Prefer per-container path. Fallback to imageFs.wineprefix (xuser
610+
// symlink) is kept for non-launch callers that have no container.
611+
val steamPath = container?.let { File(it.rootDir, ".wine/drive_c/Program Files (x86)/Steam") }
612+
?: File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam")
606613

607614
// Create necessary directories
608615
val userDataPath = File(steamPath, "userdata/$steamId64")
@@ -681,7 +688,6 @@ object SteamUtils {
681688
*/
682689
private fun restoreUnpackedExecutable(context: Context, steamAppId: Int) {
683690
try {
684-
val imageFs = ImageFs.find(context)
685691
val appDirPath = SteamService.getAppDirPath(steamAppId)
686692

687693
// Convert to Wine path format
@@ -697,8 +703,11 @@ object SteamUtils {
697703
}
698704
val executableFile = "$drive:\\${executablePath}"
699705

700-
val exe = File(imageFs.wineprefix + "/dosdevices/" + executableFile.replace("A:", "a:").replace('\\', '/'))
701-
val unpackedExe = File(imageFs.wineprefix + "/dosdevices/" + executableFile.replace("A:", "a:").replace('\\', '/') + ".unpacked.exe")
706+
// Per-container .wine path; imageFs.wineprefix resolves through the
707+
// global xuser symlink which is unsafe for writes.
708+
val winePrefix = File(container.rootDir, ".wine").absolutePath
709+
val exe = File(winePrefix + "/dosdevices/" + executableFile.replace("A:", "a:").replace('\\', '/'))
710+
val unpackedExe = File(winePrefix + "/dosdevices/" + executableFile.replace("A:", "a:").replace('\\', '/') + ".unpacked.exe")
702711

703712
if (unpackedExe.exists()) {
704713
// Check if files are different (compare size and last modified time for efficiency)
@@ -732,10 +741,10 @@ object SteamUtils {
732741
return
733742
}
734743

735-
val imageFs = ImageFs.find(context)
736-
737-
// Create the steamapps folder structure
738-
val steamappsDir = File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam/steamapps")
744+
// Per-container path; imageFs.wineprefix resolves through the global
745+
// xuser symlink which is unsafe for writes.
746+
val container = ContainerUtils.getContainer(context, "STEAM_$steamAppId")
747+
val steamappsDir = File(container.rootDir, ".wine/drive_c/Program Files (x86)/Steam/steamapps")
739748
if (!steamappsDir.exists()) {
740749
steamappsDir.mkdirs()
741750
}
@@ -1344,8 +1353,13 @@ object SteamUtils {
13441353
}
13451354

13461355
/** Writes steam.cfg with update-inhibit keys if missing. Both real-Steam and emu paths need it present. */
1347-
fun ensureSteamCfg(imageFs: ImageFs) {
1348-
val cfgFile = File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam/steam.cfg")
1356+
fun ensureSteamCfg(imageFs: ImageFs, container: Container? = null) {
1357+
// Per-container path; imageFs.wineprefix resolves through the global
1358+
// xuser symlink which is unsafe for writes. Fallback retained for
1359+
// legacy callers without a container in scope.
1360+
val cfgFile = container?.let {
1361+
File(it.rootDir, ".wine/drive_c/Program Files (x86)/Steam/steam.cfg")
1362+
} ?: File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam/steam.cfg")
13491363
if (cfgFile.exists()) return
13501364
try {
13511365
cfgFile.parentFile?.mkdirs()
@@ -1357,9 +1371,11 @@ object SteamUtils {
13571371
}
13581372

13591373
/** Logs size + mtime of the Steam binaries whose unexpected change breaks launches. Diagnostic-only. */
1360-
fun logSteamBinaryFingerprint(imageFs: ImageFs, tag: String) {
1374+
fun logSteamBinaryFingerprint(imageFs: ImageFs, tag: String, container: Container? = null) {
13611375
try {
1362-
val steamDir = File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam")
1376+
// Per-container path with symlink fallback for legacy callers.
1377+
val steamDir = container?.let { File(it.rootDir, ".wine/drive_c/Program Files (x86)/Steam") }
1378+
?: File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam")
13631379
val fmt = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).apply { timeZone = TimeZone.getDefault() }
13641380
val parts = listOf("steam.exe", "steamclient.dll", "steamclient64.dll").map { name ->
13651381
val f = File(steamDir, name)
@@ -1379,8 +1395,8 @@ object SteamUtils {
13791395
val steamAppId = ContainerUtils.extractGameIdFromContainerId(appId)
13801396
val imageFs = ImageFs.find(context)
13811397
val container = ContainerUtils.getOrCreateContainer(context, appId)
1382-
ensureSteamCfg(imageFs)
1383-
logSteamBinaryFingerprint(imageFs, "restoreSteamApi:emu:$steamAppId")
1398+
ensureSteamCfg(imageFs, container)
1399+
logSteamBinaryFingerprint(imageFs, "restoreSteamApi:emu:$steamAppId", container)
13841400

13851401
// Update or modify localconfig.vdf
13861402
updateOrModifyLocalConfig(imageFs, container, steamAppId.toString(), SteamService.userSteamId!!.accountID.toString())
@@ -1396,7 +1412,7 @@ object SteamUtils {
13961412
MarkerUtils.removeMarker(appDirPath, Marker.STEAM_COLDCLIENT_USED)
13971413

13981414
autoLoginUserChanges(imageFs, container)
1399-
setupLightweightSteamConfig(imageFs, SteamService.userSteamId!!.accountID.toString())
1415+
setupLightweightSteamConfig(imageFs, SteamService.userSteamId!!.accountID.toString(), container)
14001416

14011417
putBackSteamDlls(appDirPath)
14021418
restoreOriginalExecutable(context, steamAppId)
@@ -1488,8 +1504,10 @@ object SteamUtils {
14881504
Timber.i("Checking directory: $appDirPath")
14891505
var restoredCount = 0
14901506

1491-
val imageFs = ImageFs.find(context)
1492-
val dosDevicesPath = File(imageFs.wineprefix, "dosdevices/a:")
1507+
// Per-container path; imageFs.wineprefix resolves through the global
1508+
// xuser symlink which is unsafe.
1509+
val container = ContainerUtils.getContainer(context, "STEAM_$steamAppId")
1510+
val dosDevicesPath = File(container.rootDir, ".wine/dosdevices/a:")
14931511

14941512
dosDevicesPath.walkTopDown().maxDepth(10)
14951513
.filter { it.isFile && it.name.endsWith(".original.exe", ignoreCase = true) }
@@ -1523,7 +1541,6 @@ object SteamUtils {
15231541
*/
15241542
fun migrateGSESavesToSteamUserdata(context: Context, appId: Int, isLaunchRealSteam: Boolean = true) {
15251543
if (!isLaunchRealSteam) return
1526-
val imageFs = ImageFs.find(context)
15271544
val accountId = SteamService.userSteamId?.accountID?.toInt()
15281545
?: PrefManager.steamUserAccountId.takeIf { it != 0 }
15291546

@@ -1532,15 +1549,13 @@ object SteamUtils {
15321549
return
15331550
}
15341551

1535-
val gseDir = File(
1536-
imageFs.rootDir,
1537-
"${ImageFs.WINEPREFIX}/drive_c/users/xuser/AppData/Roaming/GSE Saves/$appId"
1538-
)
1552+
// Per-container path; imageFs.rootDir + ImageFs.WINEPREFIX resolves through
1553+
// the global xuser symlink which is unsafe for writes.
1554+
val container = ContainerUtils.getContainer(context, "STEAM_$appId")
1555+
val winePrefix = File(container.rootDir, ".wine")
15391556

1540-
val steamUserdataDir = File(
1541-
imageFs.rootDir,
1542-
"${ImageFs.WINEPREFIX}/drive_c/Program Files (x86)/Steam/userdata/$accountId/$appId"
1543-
)
1557+
val gseDir = File(winePrefix, "drive_c/users/xuser/AppData/Roaming/GSE Saves/$appId")
1558+
val steamUserdataDir = File(winePrefix, "drive_c/Program Files (x86)/Steam/userdata/$accountId/$appId")
15441559

15451560
fun isDirectoryEmpty(file: File): Boolean {
15461561
return file.isDirectory && file.list()?.isEmpty() ?: true
@@ -1613,7 +1628,6 @@ object SteamUtils {
16131628
*/
16141629
fun migrateSteamUserdataToGSESaves(context: Context, appId: Int, isLaunchRealSteam: Boolean = false) {
16151630
if (isLaunchRealSteam) return
1616-
val imageFs = ImageFs.find(context)
16171631
val accountId = SteamService.userSteamId?.accountID?.toInt()
16181632
?: PrefManager.steamUserAccountId.takeIf { it != 0 }
16191633

@@ -1622,14 +1636,13 @@ object SteamUtils {
16221636
return
16231637
}
16241638

1625-
val steamUserdataDir = File(
1626-
imageFs.rootDir,
1627-
"${ImageFs.WINEPREFIX}/drive_c/Program Files (x86)/Steam/userdata/$accountId/$appId"
1628-
)
1629-
val gseDir = File(
1630-
imageFs.rootDir,
1631-
"${ImageFs.WINEPREFIX}/drive_c/users/xuser/AppData/Roaming/GSE Saves/$appId"
1632-
)
1639+
// Per-container path; imageFs.rootDir + ImageFs.WINEPREFIX resolves through
1640+
// the global xuser symlink which is unsafe.
1641+
val container = ContainerUtils.getContainer(context, "STEAM_$appId")
1642+
val winePrefix = File(container.rootDir, ".wine")
1643+
1644+
val steamUserdataDir = File(winePrefix, "drive_c/Program Files (x86)/Steam/userdata/$accountId/$appId")
1645+
val gseDir = File(winePrefix, "drive_c/users/xuser/AppData/Roaming/GSE Saves/$appId")
16331646

16341647
fun isDirectoryEmpty(file: File): Boolean {
16351648
return file.isDirectory && file.list()?.isEmpty() ?: true
@@ -1716,10 +1729,12 @@ object SteamUtils {
17161729
})
17171730
}
17181731

1719-
fun cleanupExtractedSteamFiles(context: Context) {
1732+
fun cleanupExtractedSteamFiles(context: Context, container: Container? = null) {
17201733
try {
1721-
val imageFs = ImageFs.find(context)
1722-
val steamDir = File(imageFs.rootDir, ImageFs.WINEPREFIX + "/drive_c/Program Files (x86)/Steam")
1734+
// Per-container path; imageFs.WINEPREFIX resolves through the
1735+
// global xuser symlink which is unsafe for writes.
1736+
val steamDir = container?.let { File(it.rootDir, ".wine/drive_c/Program Files (x86)/Steam") }
1737+
?: File(ImageFs.find(context).rootDir, ImageFs.WINEPREFIX + "/drive_c/Program Files (x86)/Steam")
17231738
if (!steamDir.exists()) return
17241739
val steamExe = File(steamDir, "steam.exe")
17251740
if (!steamExe.exists()) return
@@ -1737,9 +1752,11 @@ object SteamUtils {
17371752
* Deleting userdata/<steamId>/<phantomAppId>/ breaks the association so
17381753
* shutdown completes promptly for subsequent real-Steam launches.
17391754
*/
1740-
fun purgePhantomAppUserdata(imageFs: ImageFs, phantomAppId: String) {
1755+
fun purgePhantomAppUserdata(imageFs: ImageFs, phantomAppId: String, container: Container? = null) {
17411756
try {
1742-
val userdataRoot = File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam/userdata")
1757+
// Per-container path with symlink fallback for legacy callers.
1758+
val userdataRoot = container?.let { File(it.rootDir, ".wine/drive_c/Program Files (x86)/Steam/userdata") }
1759+
?: File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam/userdata")
17431760
if (!userdataRoot.isDirectory) return
17441761
val victims = userdataRoot.listFiles { f -> f.isDirectory }?.mapNotNull { userDir ->
17451762
File(userDir, phantomAppId).takeIf { it.exists() }
@@ -2126,7 +2143,9 @@ object SteamUtils {
21262143
try {
21272144
val exeCommandLine = container.execArgs
21282145

2129-
val steamPath = File(imageFs.wineprefix, "drive_c/Program Files (x86)/Steam")
2146+
// Per-container path; imageFs.wineprefix resolves through the global
2147+
// xuser symlink which is unsafe for writes.
2148+
val steamPath = File(container.rootDir, ".wine/drive_c/Program Files (x86)/Steam")
21302149

21312150
// Create necessary directories
21322151
val userDataPath = File(steamPath, "userdata/$steamUserId64")

0 commit comments

Comments
 (0)