/* * SPDX-FileCopyrightText: 2026 Shiham Abdul Rahman * 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 )