Stores: refuse an absolute exe in a sidecar instead of trimming it; developer notes and the README line

This commit is contained in:
The412Banner committed 2026-10-08 17:11:03 -04:00
1 parent 4bcfca58e2
commit b5ae6cc63f
3 files changed
+98 -4

No files matched your search

+1 -1
View File
@@ -15,7 +15,7 @@ DroidDeck brings the SteamOS experience to Android: Valve's Steam client in Big
## Requirements and install
Use Android 9 or newer on a supported Adreno device (730 or newer, or 8xx). Adreno 6xx is experimental: DirectX 11 uses DXVK 2 and may run, and DirectX 12 games can still crash. Mali, Xclipse, PowerVR, and Adreno 710 are unsupported. No root is required. Allow about 3 GB for the runtime and 1.1 GB more for the desktop and emulators. Install the APK from [Releases](https://github.com/Droid-Deck/DroidDeck/releases), install the Linux runtime, then press **Play** and sign in. Steam downloads on first launch. Install **Desktop & apps** to use the desktop and emulators. The **Store** installs Linux apps and games from Flathub (ARM64 builds) with Flatpak; with additional options to install Appimages and set up scripts.
Use Android 9 or newer on a supported Adreno device (730 or newer, or 8xx). Adreno 6xx is experimental: DirectX 11 uses DXVK 2 and may run, and DirectX 12 games can still crash. Mali, Xclipse, PowerVR, and Adreno 710 are unsupported. No root is required. Allow about 3 GB for the runtime and 1.1 GB more for the desktop and emulators. Install the APK from [Releases](https://github.com/Droid-Deck/DroidDeck/releases), install the Linux runtime, then press **Play** and sign in. Steam downloads on first launch. Install **Desktop & apps** to use the desktop and emulators. The **Store** installs Linux apps and games from Flathub (ARM64 builds) with Flatpak; with additional options to install Appimages and set up scripts. **Stores** (Setup › Stores › "Show Stores in the rail") signs in to GOG, Epic Games and Amazon Games, browses their catalogs and libraries, downloads games into the Games storage and adds each one to the Steam client as a non-Steam game, so it launches through Proton like everything else; the cog on its chip row sets which tab a store opens on, whether installs go to Steam, and the download speed tier.
Before Steam launches, you must turn off **Restrict child processes** in Developer options. If this option is not available in developer settings (Android 12 and 13 devices), first launch of Steam will present a "Fix it for me" button, which will help automate the setup process.
@@ -88,11 +88,11 @@ class StoreGameSidecar(
val store = Store.byId(o.optString("store")) ?: return null
val id = o.optString("id", "")
val title = o.optString("title", "")
val exe = o.optString("exe", "").replace('\\', '/').trimStart('/')
val exe = o.optString("exe", "").replace('\\', '/')
if (id.isEmpty() || title.isEmpty() || exe.isEmpty()) return null
// A relative path only: a sidecar that points outside its folder is not trusted.
if (!relativeInside(exe)) return null
val launcher = o.optString("launcher", "").replace('\\', '/').trimStart('/').takeIf { it.isNotEmpty() && relativeInside(it) }
val launcher = o.optString("launcher", "").replace('\\', '/').takeIf { it.isNotEmpty() && relativeInside(it) }
val args = o.optJSONArray("args")?.let { a -> List(a.length()) { a.optString(it) }.filter { it.isNotEmpty() } } ?: emptyList()
val env = LinkedHashMap<String, String>()
o.optJSONObject("env")?.let { e -> e.keys().forEach { k -> env[k] = e.optString(k) } }
@@ -109,7 +109,8 @@ class StoreGameSidecar(
)
}
/** Relative, no drive letter, no step up: an absolute or escaping path is refused, not trimmed into shape. */
private fun relativeInside(path: String): Boolean =
path.isNotEmpty() && !path.startsWith("/") && !path.contains(":") && path.split('/').none { it == ".." }
path.isNotEmpty() && !path.startsWith("/") && !path.contains(":") && path.split('/').none { it == ".." || it.isEmpty() }
}
}
+93
View File
@@ -0,0 +1,93 @@
# Stores: GOG, Epic Games and Amazon Games in the launcher
How the Stores section (`app/src/main/java/com/droiddeck/launcher/stores/`, `ui/Stores*.kt`) fits
the rest of the app, and the on-disk contract that makes a store game an ordinary added game.
## Where a store game lives
```
<Games storage>/Games/<Store>/<title>/
<game files>
.droiddeck-store.json the sidecar (below)
.droiddeck-launch.bat only when the game needs arguments or environment (Epic, Amazon)
.droiddeck-epic-code one-shot, written right before an Epic launch, read and deleted by the .bat
```
`<Games storage>` is the second Steam library when one is chosen (the SD card's app folder, bound
into the session at `/mnt/droiddeck-sd`), else the runtime's own tree, where the folder is
`/root/Games/Stores` (`StoreInstallRoot`). Both roots are always scanned; new installs go to the
current one. `<Store>` is `GOG`, `Epic` or `Amazon`; `<title>` is the title with unsafe characters
dropped, at most 60 characters (`StoreInstallRoot.folderName`), chosen once and read back from the
sidecar afterwards.
## The sidecar
```json
{
"version": 1,
"store": "epic", // gog | epic | amazon
"id": "Samorost3", // the store's own id: GOG product id, Epic app name, Amazon product id
"title": "Samorost 3",
"exe": "Samorost3.exe", // relative to the folder, forward slashes; what the icon is read from
"launcher": ".droiddeck-launch.bat",// optional; what the shortcut runs instead of exe
"args": ["-EpicPortal", "-epicusername=\"Name\"", "..."],
"env": {"FUEL_DIR": "C:\\ProgramData\\Amazon Games Services\\Legacy"},
"installVersion": "1.4.0",
"installedAt": 1759900000000,
"addToSteam": true, // false keeps the game out of the Steam client (and the Games tab)
"cover": "https://...", "hero": "https://...",
"extra": {"namespace": "...", "catalogItemId": "..."}
}
```
`StoreGameSidecar.parse` refuses anything whose `exe` or `launcher` would point outside the folder.
## Registration with Steam
`AddedGames.scan` walks the store folders as it walks the user's added folders, and
`AddedGames.scanGame` reads the sidecar: the exe (the folder's guess only when that file is gone),
the title as the shortcut's name, the store as the game's `source`. From there the path is the one
an added folder takes: `session/added-games.json` → `droiddeck-steam-shortcuts` at the client's
next start. `StoreInstalls.register` rewrites the listing at install time, so a client that restarts
inside the running session already has it, and asks a running client to add the shortcut live over
its DevTools port (`SteamLiveShortcuts`: `SteamClient.Apps.AddShortcut`, our tag, the compat tool).
The appid Steam derives (CRC32 of the quoted exe plus the name, high bit set) is the one the
listing carries, so the live add and the writer never disagree. Uninstall removes the folder and the
listing drops the entry; the live client is asked to remove it too.
`Library.SteamGame.source` is `steam`, `gog`, `epic`, `amazon` or `added`; the Games tab shows it as
a chip on every row and in the hero.
## Launch
A Steam shortcut names an exe and nothing more, and the listing carries no launch options, so a
game that needs arguments or environment gets `.droiddeck-launch.bat` (`StoreLaunch.launcherText`):
`cd` into the exe's folder, `set` each variable, start the exe with the arguments and wait. Proton's
`steam.exe` shim hands a `.bat` to Wine's `cmd`. For Epic the script also reads
`.droiddeck-epic-code` when present, deletes it, and appends
`-AUTH_LOGIN=unused -AUTH_PASSWORD=<code> -AUTH_TYPE=exchangecode`; `StoreLaunch.prepare` mints the
code right before a launch from the Stores page or the Games tab (the Steam client's own Play
button gets the offline identity arguments only).
## Downloads
`DownloadQueue` runs 1–3 jobs at a time (Setup / the Downloads page), each a store's whole install
(`DownloadJob.run`), with stages Manifest → Download → Verify → Install. Pause stops the job and
keeps the files; every store's install skips complete, verified files on the rerun, so resume is a
rerun. Cancel deletes the folder. `StoreDownloadService` holds a foreground notification while
anything runs. The native engine (`libdroiddeckstores.so`, `StoresNative`, `GogNative`, `EpicNative`,
`AmazonNative`; contract in `app/src/main/rust/stores/JNI.md`) is loaded on first use; without it
each manager runs its Java fetch loop.
## Settings
| Setting | Pref | Where |
|---|---|---|
| Show Stores in the rail | `SessionPrefs.gameStoresEnabled` (off) | Setup › Stores |
| Open a store on: Library · Store | `SessionPrefs.storesOpenTab` (library) | the chip row's cog, Setup › Stores |
| Add installed store games to Steam | `SessionPrefs.gameStoresAddToSteam` (on) | the cog, Setup › Stores |
| Download speed tier | `SessionPrefs.gameStoresSpeedTier` (fast) | the cog, Setup › Stores, Downloads |
| Downloads at a time | `SessionPrefs.gameStoresParallel` (1) | Downloads |
Credentials live in `filesDir/stores/<store>/credentials.json` only (`StoreAccounts`); store URLs
are logged through `StoreLog.redactUrl`, which drops the query string where the signed tokens live.