mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4bd2af28e | ||
|
|
7962c19283 | ||
|
|
7f141314c6 | ||
|
|
4fc2162203 | ||
|
|
3bfe004d44 | ||
|
|
0240c75dbd | ||
|
|
0d1d47e8b0 | ||
|
|
14031c6911 |
No files matched your search
@@ -6,6 +6,7 @@ import java.nio.file.LinkOption.NOFOLLOW_LINKS
|
||||
import java.nio.file.StandardCopyOption.COPY_ATTRIBUTES
|
||||
import java.security.MessageDigest
|
||||
import java.util.Locale
|
||||
import java.util.UUID
|
||||
|
||||
/** Android shared storage cannot create the links in Wine prefixes or native Steam depots. */
|
||||
object SecondaryLibrary {
|
||||
@@ -17,6 +18,30 @@ object SecondaryLibrary {
|
||||
"3127680" to "FEX-Emu",
|
||||
)
|
||||
|
||||
/** Some devices permit direct access to the backing volume; others require Android's FUSE path. */
|
||||
fun contentPath(library: File): File {
|
||||
val path = library.absolutePath
|
||||
if (!path.startsWith("/storage/")) return library
|
||||
return verifiedAlias(library, File("/mnt/media_rw/" + path.removePrefix("/storage/")))
|
||||
}
|
||||
|
||||
internal fun verifiedAlias(library: File, candidate: File): File {
|
||||
if (!candidate.isDirectory || !candidate.canRead() || !candidate.canWrite()) return library
|
||||
var probe: File? = null
|
||||
return try {
|
||||
val created = File.createTempFile(".droiddeck-path-", ".probe", candidate)
|
||||
probe = created
|
||||
val token = UUID.randomUUID().toString()
|
||||
created.writeText(token)
|
||||
val visible = File(library, created.name)
|
||||
if (visible.readText() == token && visible.delete() && !created.exists()) candidate else library
|
||||
} catch (_: Exception) {
|
||||
library
|
||||
} finally {
|
||||
probe?.delete()
|
||||
}
|
||||
}
|
||||
|
||||
fun privateRoot(files: File, library: File): File {
|
||||
val digest = MessageDigest.getInstance("SHA-256").digest(library.canonicalPath.toByteArray())
|
||||
.joinToString("") { "%02x".format(it.toInt() and 255) }
|
||||
@@ -24,7 +49,7 @@ object SecondaryLibrary {
|
||||
}
|
||||
|
||||
/** Parent binds come first; the more specific private directories override them in PRoot. */
|
||||
fun binds(files: File, library: File): List<String> {
|
||||
fun binds(files: File, library: File, content: File = library): List<String> {
|
||||
val private = privateRoot(files, library)
|
||||
val apps = File(library, "steamapps")
|
||||
val tools = LinkedHashMap(bootstrapTools)
|
||||
@@ -58,7 +83,7 @@ object SecondaryLibrary {
|
||||
}
|
||||
}
|
||||
return listOf("/mnt/droiddeck-sd", "/mnt/bannerlator-sd").flatMap { guest ->
|
||||
listOf("${library.path}:$guest") + overrides.map { (host, relative) -> "${host.path}:$guest/$relative" }
|
||||
listOf("${content.path}:$guest") + overrides.map { (host, relative) -> "${host.path}:$guest/$relative" }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -799,9 +799,10 @@ class SessionService : Service() {
|
||||
// Links, prefixes and Steam entries made before the rename still name the old path.
|
||||
File(LinuxRuntime.rootDir(this), "mnt/bannerlator-sd").mkdirs()
|
||||
try {
|
||||
binds.addAll(SecondaryLibrary.binds(filesDir, File(library.path)))
|
||||
storageDiagnosticLibrary = File(library.path)
|
||||
Log.i(TAG, "game storage: ${library.path} -> /mnt/droiddeck-sd (\"${library.label}\"); prefixes and native tools private")
|
||||
val content = SecondaryLibrary.contentPath(File(library.path))
|
||||
binds.addAll(SecondaryLibrary.binds(filesDir, File(library.path), content))
|
||||
storageDiagnosticLibrary = content
|
||||
Log.i(TAG, "game storage: ${content.path} -> /mnt/droiddeck-sd (\"${library.label}\"); prefixes and native tools private")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "game storage: private directories could not be prepared; internal only this session", e)
|
||||
}
|
||||
@@ -816,9 +817,9 @@ class SessionService : Service() {
|
||||
val device = StorageDiagnostics.writeSnapshot(
|
||||
sessionDir, target,
|
||||
selectedLibrary = if (storageDiagnosticLibrary != null) "secondary" else "internal",
|
||||
removable = storageDiagnosticLibrary?.let {
|
||||
runCatching { Environment.isExternalStorageRemovable(it) }.getOrDefault(false)
|
||||
} ?: false,
|
||||
removable = storageDiagnosticLibrary != null && library?.let {
|
||||
runCatching { Environment.isExternalStorageRemovable(File(it.path)) }.getOrDefault(false)
|
||||
} == true,
|
||||
)
|
||||
if (device != null) {
|
||||
// guest already has the session command at this point. Put these with the `env -i`
|
||||
|
||||
@@ -10,6 +10,34 @@ import org.junit.rules.TemporaryFolder
|
||||
class SecondaryLibraryTest {
|
||||
@get:Rule val tmp = TemporaryFolder()
|
||||
|
||||
@Test fun directContentKeepsTheConfiguredLibrarysPrivateDirectories() {
|
||||
val files = tmp.newFolder("files")
|
||||
val library = tmp.newFolder("shared")
|
||||
val content = tmp.newFolder("direct")
|
||||
val private = SecondaryLibrary.privateRoot(files, library)
|
||||
val binds = SecondaryLibrary.binds(files, library, content)
|
||||
assertTrue(binds.contains("${content.path}:/mnt/droiddeck-sd"))
|
||||
assertTrue(binds.contains("$private/steamapps/compatdata:/mnt/droiddeck-sd/steamapps/compatdata"))
|
||||
assertFalse(SecondaryLibrary.privateRoot(files, content).exists())
|
||||
}
|
||||
|
||||
@Test fun aWritableAliasIsSelectedAndItsProbeIsRemoved() {
|
||||
val library = tmp.newFolder("shared")
|
||||
val candidate = File(tmp.root, "direct")
|
||||
Files.createSymbolicLink(candidate.toPath(), library.toPath())
|
||||
assertEquals(candidate, SecondaryLibrary.verifiedAlias(library, candidate))
|
||||
assertTrue(library.listFiles()!!.isEmpty())
|
||||
}
|
||||
|
||||
@Test fun anUnrelatedOrMissingDirectoryCannotReplaceTheLibrary() {
|
||||
val library = tmp.newFolder("shared")
|
||||
val candidate = tmp.newFolder("other")
|
||||
assertEquals(library, SecondaryLibrary.verifiedAlias(library, candidate))
|
||||
assertTrue(candidate.listFiles()!!.isEmpty())
|
||||
assertTrue(library.listFiles()!!.isEmpty())
|
||||
assertEquals(library, SecondaryLibrary.verifiedAlias(library, File(tmp.root, "missing")))
|
||||
}
|
||||
|
||||
@Test fun gamesStaySharedButPrefixesAndNativeDownloadsArePrivate() {
|
||||
val files = tmp.newFolder("files")
|
||||
val library = tmp.newFolder("card")
|
||||
|
||||
@@ -9,14 +9,15 @@
|
||||
*
|
||||
* It decides nothing proot would decide differently. The guest path is mapped literally - the
|
||||
* longest binding whose guest path is a prefix, else the rootfs - and the parent directory is
|
||||
* opened O_PATH and must come back from /proc/self/fd as exactly that host path, which proves no
|
||||
* component on the way was a symlink: proot would then have walked the same directories. The
|
||||
* last component is looked at without following it, and a symlink there goes to proot. So does
|
||||
* "..", a trailing "/", anything under /proc, any flag not handled here and any failure to
|
||||
* decide: the real libc call runs, proot traps it and answers as it always has.
|
||||
* opened O_PATH and checked against /proc/self/fd. A case-only difference is accepted only on
|
||||
* FUSE or FAT and only after each differently spelled component is verified as a directory rather than
|
||||
* a symlink. The last component is looked at without following it; other symlinks go to proot.
|
||||
* So do "..", a trailing "/", anything under /proc, any flag not handled here and any failure
|
||||
* to decide: the real libc call runs, proot traps it and answers as it always has.
|
||||
*
|
||||
* Only syscalls Android's app seccomp policy allows are used (openat, newfstatat, statx,
|
||||
* faccessat, readlinkat, close) - not openat2 or faccessat2, which it answers with SIGSYS.
|
||||
* faccessat, fstatfs, readlinkat, close) - not openat2 or faccessat2, which it answers with
|
||||
* SIGSYS.
|
||||
*
|
||||
* Verified parents are remembered for PROOT_FP_TTL_MS (default 2000; 0 = verify every call): a
|
||||
* parent directory replaced by a symlink within that window would be missed, the same trade the
|
||||
@@ -33,6 +34,7 @@
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <linux/magic.h>
|
||||
#include <pthread.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
@@ -43,6 +45,8 @@
|
||||
#include <sys/stat.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <sys/sysmacros.h>
|
||||
#include <sys/vfs.h>
|
||||
#include <strings.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
@@ -250,10 +254,28 @@ static long canonical_dir(const char *dir) {
|
||||
char link[40], out[PATH_MAX];
|
||||
snprintf(link, sizeof link, "/proc/self/fd/%ld", fd);
|
||||
long n = sc(SYS_readlinkat, AT_FDCWD, (long)link, (long)out, sizeof out - 1, 0);
|
||||
sc(SYS_close, fd, 0, 0, 0, 0);
|
||||
if (n <= 0) return 0;
|
||||
if (n <= 0) { sc(SYS_close, fd, 0, 0, 0, 0); return 0; }
|
||||
out[n] = 0;
|
||||
if (strcmp(out, dir) != 0) return 0;
|
||||
if (strcmp(out, dir) != 0) {
|
||||
struct statfs fs;
|
||||
int case_alias = sc(SYS_fstatfs, fd, (long)&fs, 0, 0, 0) == 0 &&
|
||||
(fs.f_type == FUSE_SUPER_MAGIC || fs.f_type == EXFAT_SUPER_MAGIC || fs.f_type == MSDOS_SUPER_MAGIC);
|
||||
sc(SYS_close, fd, 0, 0, 0, 0);
|
||||
if (!case_alias || strcasecmp(out, dir) != 0) return 0;
|
||||
/* FUSE and FAT case aliases return the stored spelling from /proc/self/fd. Prove the final
|
||||
* component and each parent so a case-only symlink path still goes to proot. */
|
||||
struct stat st;
|
||||
if (sc(SYS_newfstatat, AT_FDCWD, (long)dir, (long)&st, AT_SYMLINK_NOFOLLOW, 0) != 0 ||
|
||||
!S_ISDIR(st.st_mode)) return 0;
|
||||
char parent[PATH_MAX];
|
||||
snprintf(parent, sizeof parent, "%s", dir);
|
||||
char *slash = strrchr(parent, '/');
|
||||
if (!slash || slash == parent) return 0;
|
||||
*slash = 0;
|
||||
if (canonical_dir(parent) != 1) return 0;
|
||||
} else {
|
||||
sc(SYS_close, fd, 0, 0, 0, 0);
|
||||
}
|
||||
remember(dir, h);
|
||||
return 1;
|
||||
}
|
||||
@@ -474,14 +496,16 @@ static int access_common(int dirfd, const char *path, int mode, int flags) {
|
||||
REAL(faccessat, int (*)(int, const char *, int, int));
|
||||
char host[PATH_MAX];
|
||||
struct stat st;
|
||||
/* faccessat(2) has no flags: AT_EACCESS and AT_SYMLINK_NOFOLLOW stay with glibc and proot. */
|
||||
long rs = flags == 0 ? resolve(dirfd, path, host) : FP_SLOW;
|
||||
/* F_OK with AT_SYMLINK_NOFOLLOW asks only whether the last directory entry exists. */
|
||||
int nofollow_exists = mode == F_OK && flags == AT_SYMLINK_NOFOLLOW;
|
||||
long rs = flags == 0 || nofollow_exists ? resolve(dirfd, path, host) : FP_SLOW;
|
||||
if (rs == -ENOENT) { hits++; return ret(rs); }
|
||||
if (rs == 0) {
|
||||
long k = kind(host, &st);
|
||||
if (k != 1) {
|
||||
if (k != 1 || nofollow_exists) {
|
||||
hits++;
|
||||
if (k < 0) return ret(k);
|
||||
if (nofollow_exists) return 0;
|
||||
return ret(sc(SYS_faccessat, AT_FDCWD, (long)host, mode, 0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user