97 lines
2.9 KiB
Kotlin
97 lines
2.9 KiB
Kotlin
/*
|
|
* SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman <shihaam@shihaam.dev>
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*/
|
|
|
|
package sh.sar.isodroid.root
|
|
|
|
import com.topjohnwu.superuser.Shell
|
|
import sh.sar.isodroid.BuildConfig
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.withContext
|
|
|
|
object RootManager {
|
|
|
|
/**
|
|
* Escapes a string for safe use in shell commands.
|
|
* Uses single quotes and escapes any single quotes within the string.
|
|
* This prevents command injection via $(), ``, ;, &&, ||, etc.
|
|
*/
|
|
fun shellEscape(s: String): String {
|
|
// Single quotes prevent all shell interpretation except for single quotes themselves
|
|
// To include a single quote, we end the single-quoted string, add an escaped single quote, and start a new single-quoted string
|
|
// Example: "test'file" becomes 'test'\''file'
|
|
return "'" + s.replace("'", "'\\''") + "'"
|
|
}
|
|
|
|
init {
|
|
Shell.enableVerboseLogging = BuildConfig.DEBUG
|
|
Shell.setDefaultBuilder(
|
|
Shell.Builder.create()
|
|
.setFlags(Shell.FLAG_MOUNT_MASTER)
|
|
.setTimeout(10)
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Check if root was previously granted (cached status, no popup).
|
|
* Returns true if granted, false if denied, null if unknown.
|
|
*/
|
|
fun isRootGrantedCached(): Boolean? {
|
|
return Shell.isAppGrantedRoot()
|
|
}
|
|
|
|
/**
|
|
* Check if root is available. Will trigger Magisk popup if not previously decided.
|
|
*/
|
|
suspend fun hasRoot(): Boolean = withContext(Dispatchers.IO) {
|
|
try {
|
|
// getShell() initializes the shell and checks root status with Magisk
|
|
// If already granted, Magisk will auto-approve silently
|
|
Shell.getShell().isRoot
|
|
} catch (e: Exception) {
|
|
false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Request root access. Will trigger Magisk popup.
|
|
*/
|
|
suspend fun requestRoot(): Boolean = withContext(Dispatchers.IO) {
|
|
try {
|
|
Shell.getShell().isRoot
|
|
} catch (e: Exception) {
|
|
false
|
|
}
|
|
}
|
|
|
|
suspend fun executeCommand(command: String): CommandResult = withContext(Dispatchers.IO) {
|
|
val result = Shell.cmd(command).exec()
|
|
CommandResult(
|
|
success = result.isSuccess,
|
|
output = result.out.joinToString("\n"),
|
|
error = result.err.joinToString("\n"),
|
|
exitCode = result.code
|
|
)
|
|
}
|
|
|
|
suspend fun executeCommands(vararg commands: String): CommandResult = withContext(Dispatchers.IO) {
|
|
val result = Shell.cmd(*commands).exec()
|
|
CommandResult(
|
|
success = result.isSuccess,
|
|
output = result.out.joinToString("\n"),
|
|
error = result.err.joinToString("\n"),
|
|
exitCode = result.code
|
|
)
|
|
}
|
|
|
|
fun getShell(): Shell = Shell.getShell()
|
|
}
|
|
|
|
data class CommandResult(
|
|
val success: Boolean,
|
|
val output: String,
|
|
val error: String,
|
|
val exitCode: Int
|
|
)
|